+ Shiro structures separated front and rear ends of spring boot authentication based architecture

I. Introduction

Why Shiro?

Apache Shiro is a powerful and easy to use Java security framework. Developers using shiro can easily complete the authentication, authorization, password and session management.

Shiro's main API

Authentication : authentication / login, verify that the user is not have the appropriate identification;

The Authorization : Authorization that the competence to verify, verify that a user has authenticated a privilege; that is, whether the user can do things such as common: verify that a user has a role. Or fine-grained verify whether a user has a permission for a resource;

Manager the Session : Session management, that is, after the user logs in one session, in the absence of exit, it's all the information in the session; the session can be ordinary JavaSE environment, it can be as Web environment;

Cryptography : encryption, secure data, such as encrypted passwords stored in the database, instead of storing the plaintext;

Support Web : Web support can be very easily integrated into the Web environment;

Caching : Cache, such as users log in, their user information, with roles / permissions do not have to check every time, this can increase efficiency;

Concurrency : shiro support concurrent verification multithreaded applications, such as opening that is another thread in a thread, permissions can be automatically propagated in the past;

Testing : provide test support;

Of As RUN : allows a user to pretend to another user (if they allow) the identity of access;

Me Remember : Remember me, this is a very common feature, ie after the first login, then do not come back next time logged.

How Shiro work?

Subject : subject, it represents the current "user", the user is not necessarily a specific person, and any current stuff is Subject interactive applications, such as web crawlers, robots; that is an abstract concept; all are bound to the Subject SecurityManager, all interactions with the Subject will be entrusted to the SecurityManager; Subject can be considered a facade; SecurityManager is the actual performer;

SecurityManager : security manager; that is, all safety-related operations will interact with the SecurityManager; and it manages all Subject; we can see it is the core of Shiro, which is responsible for interacting with other components behind the introduction of, if studied SpringMVC, you can see it as DispatcherServlet front controller;

Realm : domain, Shiro acquired from safety data (such as users, roles, permissions) from the Realm, that is SecurityManager to authenticate users, it needs to obtain the appropriate user from the Realm are compared to determine the identity of the user is legitimate; need from the Realm to give the user the role / permissions to verify that the user can operate; can Realm as dataSource, i.e. secure data source.

 

II. The core code

1. Login / logout function

Suppose we have two users:

 

admin1: username / password admin1 / admin on success, identity and sessionId;

admin2: username / password admin2 / admin on success, identity and sessionId;

After the required access control interface must request header header tape sessionId (Authorization: sessionId format) at http.

Login / logout Code:

@PostMapping("/login")
    public Object login(@RequestBody LoginVo loginVo) {
        //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getAccount(), loginVo.getPassword());
        token.setRememberMe(false);
        if (subject.isAuthenticated()) {
            subject.logout();
        }
        try {
            //登录,即身份验证
            subject.login(token);
            Session session = subject.getSession();
            User user = User.loginUser();
            user.setFlag(loginVo.getFlag());
            user.setSessionId(session.getId());
            //返回一个sessionId
            return user;
        } catch (UnknownAccountException e){
            return "账号/密码错误";
        }
        catch (AuthenticationException e) {
            //身份验证失败
            return "程序错误";
        }
    }

    @PostMapping("/logout")
    public Object logout() {
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            return "成功退出登录!";
        } catch (Exception e) {
            return "退出登录失败!";
        }
    }

 

2.Shiro arrangement

Shiro configured interceptor, and open @RequiresPermissions annotation support

package com.lee.config;


import com.lee.filter.ApiPathPermissionFilter;
import com.lee.shiro.MyRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 *
 * 功能描述: shiro配置类
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:05
 */
@Configuration
public class ShiroConfig {

    /**
     * 设置/login /logout 两个请求可以任意访问
     */
    @Bean
    public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setSecurityManager(securityManager);

        // 自定义拦截器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("apiPathPermissionFilter", new ApiPathPermissionFilter());
        factoryBean.setFilters(filterMap);

        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterRuleMap.put("/logout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterRuleMap.put("/login", "anon");
        // 其他请求通过我们自己的apiPathPermissionFilter
        filterRuleMap.put("/*", "apiPathPermissionFilter");
        filterRuleMap.put("/**", "apiPathPermissionFilter");
        filterRuleMap.put("/*.*", "apiPathPermissionFilter");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }

    /**
     * SecurityManager安全管理器,是Shiro的核心
     */
    @Bean
    public SecurityManager securityManager(MyRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置session生命周期
        securityManager.setSessionManager(sessionManager());
        // 设置自定义 realm
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * 自定义sessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        //由shiro管理session,每次访问后会重置过期时间
        MySessionManager mySessionManager = new MySessionManager();
        //设置过期时间,单位:毫秒
        mySessionManager.setGlobalSessionTimeout(MySessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT);
        return mySessionManager;
    }


    /**
     * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部,
     * 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


}

2.sessionId get

Achieve their own set of session manager, the need to rewrite DefaultWebSessionManager
package com.lee.config;

import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 *
 * 功能描述: 自定义sessionId获取,用于请求头中传递sessionId,并让shiro获取判断权限
 * 想实现自己的一套session管理器,需继承DefaultWebSessionManager来重写
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:05
 */
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    public MySessionManager() {
        super();
    }


    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //修改shiro管理sessionId的方式,改为获取请求头,前端时header必须带上 Authorization:sessionId
        return WebUtils.toHttp(request).getHeader(AUTHORIZATION);
    }
}

3. Log achieve and access control

doGetAuthenticationInfo is a concrete implementation login to judge by querying the database password and encryption match the account is correct

doGetAuthorizationInfo permission judgment is embodied to determine the user's request by acquiring the current user permissions

We have set permissions admin1 poetry1 poetry2 of, admin2 has poetry3 poetry4 rights

package com.lee.shiro;

import com.lee.entity.User;
import com.lee.util.PasswordUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 *
 * 功能描述:自定义用户认证授权类
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:04
 */
@Component
public class MyRealm extends AuthorizingRealm {

    @Value("${password_salt}")
    private String salt;

    /**
     * AuthorizationInfo 用于聚合授权信息
     * 会判断@RequiresPermissions 里的值是否
     *
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //进入数据库查询拥有的权限查询
        List<String> list = new ArrayList<>();
        User user = (User)SecurityUtils.getSubject().getPrincipal();
        if ("1".equals(user.getRoleId())){
            String[] array = {"poetry1","poetry2"};
            list  = Arrays.asList(array);
        }else if ("2".equals(user.getRoleId())){
            String[] array = {"poetry3","poetry4"};
            list  = Arrays.asList(array);
        }

        Set<String> set = new HashSet(list);
        info.addStringPermissions(set);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String account = (String) authenticationToken.getPrincipal();  //得到用户名
        String pwd = new String((char[]) authenticationToken.getCredentials()); //得到密码
        //将密码进行加密处理,与数据库加密后的密码进行比较
        String inPasswd = PasswordUtils.entryptPasswordWithSalt(pwd, salt);
        //通过数据库验证账号密码,成功的话返回一个封装的ShiroUser实例
        String saltPasswd = PasswordUtils.entryptPasswordWithSalt("admin", salt);
        User user = null;
        //这里要注意返回用户信息尽可能少,返回前端所需要的用户信息就可以了
        if ("admin1".equals(account) && saltPasswd.equals(inPasswd)) {
            user = new User();
            user.setUid("1");
            user.setUname("用户一");
            user.setEid(1);
            user.setDeptName("祖安大区");
            user.setDeptId("1");
            user.setRoleId("1");
            user.setRoleName("祖安文科状元");
        }else if ("admin2".equals(account) && saltPasswd.equals(inPasswd)){
            user = new User();
            user.setUid("1");
            user.setUname("用户二");
            user.setEid(1);
            user.setDeptName("祖安大区");
            user.setDeptId("1");
            user.setRoleId("2");
            user.setRoleName("祖安理科状元");
        }
        if (user != null) {
            //如果身份认证验证成功,返回一个AuthenticationInfo实现;
            return new SimpleAuthenticationInfo(user, pwd, getName());
        } else {
            //错误的帐号
            throw new UnknownAccountException();
        }
    }
}

4. Cross-domain issues

The emergence of cross-domain issues in the development process

package com.lee.config;

import com.lee.interceptor.UrlInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 *
 * 功能描述: MVC拦截器配置
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:06
 */
@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //为所有请求处理跨域问题
        registry.addInterceptor(new UrlInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

}

Cross-domain solutions

package com.lee.interceptor;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * 功能描述: url拦截器,处理前后端分离跨域问题
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:04
 */
public class UrlInterceptor extends HandlerInterceptorAdapter {

	@Override
	public boolean preHandle(HttpServletRequest request,
							 HttpServletResponse response, Object handler) throws Exception {
		//允许跨域,不能放在postHandle内
		response.setHeader("Access-Control-Allow-Credentials", "true");
		String str = request.getHeader("origin");
		response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
		response.setHeader("Cache-Control", "no-cache");
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
		response.setHeader("Access-Control-Max-Age", "0");
		response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,Authorization,WG-App-Version, WG-Device-Id, WG-Network-Type, WG-Vendor, WG-OS-Type, WG-OS-Version, WG-Device-Model, WG-CPU, WG-Sid, WG-App-Id, WG-Token");
		response.setHeader("XDomainRequestAllowed", "1");
		return true;
	}
}

5. Verify permissions

To verify permissions @RequiresPermissions (ShiroConfig class configuration)

    @GetMapping("/poetry1")
    @RequiresPermissions("poetry1")
    public Object poetry1(){
        return "床前明月光";
    }

    @GetMapping("/poetry2")
    @RequiresPermissions("poetry2")
    public Object poetry2(){
        return "疑是地上霜";
    }

    @GetMapping("/poetry3")
    @RequiresPermissions("poetry3")
    public Object poetry3(){
        return "举头望明月";
    }

    @GetMapping("/poetry4")
    @RequiresPermissions("poetry4")
    public Object poetry4(){
        return "低头思故乡";
    }

Specific code: https://github.com/leeyiyu/shiro_token

Reference documents: https://www.iteye.com/blog/jinnianshilongnian-2018398

Published an original article · won praise 1 · views 39

Guess you like

Origin blog.csdn.net/liyiyu123/article/details/105016859