spring-boot-2 + shiro + jwt achieve integrated rights management

After years of empty, I spent some time researching apache shiro. Sample Code Address

https://github.com/CodingSoldier/java-learn/tree/master/project/shiro/shiro-jwt

If you do not understand Apache shiro, I offer my learning path:

1, the first lesson to learn free tutorials Mu network " Shiro security framework Getting Started " https://www.imooc.com/learn/977 .

2, then charge tutorial lessons learned Mu network "Java development enterprise rights management system," only learned Chapter 3 Apache Shiro rights framework theory and practical exercises https://coding.imooc.com/class/chapter/149.html #anchor .

3, the last school shiro documents, 12 have a Chinese version of the document, a translation level is not very good, a little better than the google translation bar. https://www.ctolib.com/docs/sfile/apache-shiro-reference/

spring-boot-2 + shiro + jwt integration

pom.xml dependence

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--spring-boot与shiro的整合包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

        <!--jwt依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

shiro specific subject, principals:

subject is a security term, refers to a particular user of a secure application "view." Shiro Subject instance represents a safe state of a single application and user operation. For web applications, in most cases subject is the user, it may be a client.

principals (identity) is Subject of "identity property." For example: userId, username, client id. In another security framework spring security principals are also used to uniquely identify the user.

May be understood from the following code

    @Test
    public void test1(){

        SimpleAccountRealm sar = new SimpleAccountRealm();
        sar.addAccount("username01", "pwd01");

        DefaultSecurityManager dsm = new DefaultSecurityManager();
        dsm.setRealm(sar);

        SecurityUtils.setSecurityManager(dsm);
        Subject subject = SecurityUtils.getSubject();
        System.out.println("subject未登陆,principals是null: "+subject.getPrincipals());

        UsernamePasswordToken token = new UsernamePasswordToken("username01", "pwd01");

        subject.login(token);
        System.out.println("subject登陆成功后,principals是UsernamePasswordToken中的第一个参数: "+subject.getPrincipals());

    }

Why after the successful landing subject of identity attributes principals are UsernamePasswordToken the username of it?

Because UsernamePasswordToken source interface cover AuthenticationToken getPrincipal () method

public Object getPrincipal() {
    return this.getUsername();
}

This method returns the username UsernamePasswordToken first parameter.

Use default UsernamePasswordToken landing, shiro will principals and subject of certification status is stored in the session, but the session has java is less common, and more commonly used is token. It will be used herein jwt generated token.

There are several concepts you need to know:

shiro user authentication and user authorization are separate. User authentication (can be understood as the landing) called the Authentication, Authorization user called Authorization, which looks kinda like two words. When hit with a class or method would indicate Authentication and related authentication, authorization, and it indicates encountered Authorization related.

Realm is stored in the client data security as a component of users, roles, permissions, etc., it can be understood as DAO.

shiro first with the filter intercepts the request and then calls realm to obtain user authentication, authorization information.

Here are some highlights of code

subject.login (token) received parameter method is AuthenticationToken, a new inheritance JwtToken AuthenticationToken.

public class JwtToken implements AuthenticationToken {
    private String token;
    public JwtToken(String token) {
        this.token = token;
    }

    // token作为principal,
    @Override
    public Object getPrincipal() {
        return this.token;
    }

    // 由于getPrincipal没返回username,而是返回token,所以credentials设置为空字符串即可
    @Override
    public Object getCredentials() {
        return Constant.CREDENTIALS_EMPTY;
    }
}

New JwtFilter inheritance BasicHttpAuthenticationFilter, see Authentication word, everyone should know the role of this filter is user authentication.

@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        // 获取请求头Authorization的值
        String authorization = getAuthzHeader(request);
        return new JwtToken(authorization);
    }

    /**
     * 执行登录操作
     * 大部分代码跟父类一样,不同之处是catch到异常后返回自定义异常给前端
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        } else {
            try {
                Subject subject = this.getSubject(request, response);
                subject.login(token);
                return this.onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException e) {
                Result result = Result.fail("用户认证异常");
                if (e.getCause() instanceof CustomException){
                    CustomException customException = (CustomException)e.getCause();
                    result = Result.fail(customException.getCode(), customException.getMessage());
                }
                WebUtil.sendResponse(response, result);
                return false;
            }
        }
    }

    /**
     * 身份认证未通过,执行此方法
     * 返回true,继续处理请求
     * 返回false,不继续处理请求,结束过滤器链
     * BasicHttpAuthenticationFilter源码中也是在onAccessDenied()方法内调用executeLogin
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        boolean r;
        String authorization = getAuthzHeader(request);
        if (StringUtils.isEmpty(authorization)){
            WebUtil.sendResponse(response, Result.fail(Constant.CODE_TOKEN_ERROR,"无token"));
            r = false;
        }else {
            r = executeLogin(request, response);
        }
        return r;
    }
    
}

shiro default permissions filter is PermissionsAuthorizationFilter, when the user does not have access to the interface, information is returned to the front of the unfriendly, we write a CustomPermissionsAuthorizationFilter inherited PermissionsAuthorizationFilter return a custom message to the front. Authorization see this word, you know that this filter is associated with the authorized user.

@Slf4j
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

    /**
     * 用户无权访问url时,此方法会被调用
     * 默认实现为org.apache.shiro.web.filter.authz.AuthorizationFilter#onAccessDenied()
     * 覆盖父类的方法,返回自定义信息给前端
     *
     * 接口doc上说:
     *    AuthorizationFilter子类(权限授权过滤器)的onAccessDenied()应该永远返回false,那么在onAccessDenied()内就必然要发送response响应给前端,不然前端就收不到任何数据
     *    AuthenticationFilter、AuthenticatingFilter子类(身份认证过滤器)的onAccessDenied()的返回值则没有限制
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        WebUtil.sendResponse(response, Result.fail("权限不足"));
        return false;
    }

}

To build a JwtRealm

public class JwtRealm extends AuthorizingRealm {

    private UserService userService;

    /**
     * 最好保持单例
     * JwtRealm可以不交给spring管理,在创建JwtRealm的时候需要创建者传递参数UserService
     */
    public JwtRealm(UserService userService) {
        this.userService = userService;

        /**
         * 启动认证缓存,默认是false。源码如下
         * org.apache.shiro.realm.AuthenticatingRealm#AuthenticatingRealm()
         *     this.authenticationCachingEnabled = false;
         */
        this.setAuthenticationCachingEnabled(true);

        /**
         * 启动授权缓存,默认就是true,代码如下
         * org.apache.shiro.realm.AuthorizingRealm#AuthorizingRealm()
         *     this.authorizationCachingEnabled = true;
         */
        // this.setAuthorizationCachingEnabled(true);

        // 设置缓存管理器,使用shiro自带的MemoryConstrainedCacheManager即可
        this.setCacheManager(new MemoryConstrainedCacheManager());

    }

    // subject.login(token)方法中的token是JwtToken时,调用此Realm的doGetAuthenticationInfo
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 认证用户
     * 本方法被 org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo()调用
     *    AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
     *    如果通过token在缓存中获取到用户认证,就不调用本方法
     *
     * 补充一点:用户认证接口往往只传递principals信息,不传credentials。在spring security中也是这种思路
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws CustomAuthenticationException {
        String token = (String) authenticationToken.getPrincipal();
        if (StringUtils.isEmpty(token)){
            throw new CustomAuthenticationException("token为空");
        }
        String username = JWTUtil.getUsername(token);
        User user = userService.getUser(username);
        if (user == null){
            throw new CustomAuthenticationException("无此用户");
        }
        /**
         * token无效,抛出异常
         * MyControllerAdvice捕获MyException异常后,将Constant.CODE_TOKEN_ERROR返回给前端,前端收到此code后跳转登录页
         */
        if (!JWTUtil.verify(token, username, user.getPassword())){
            throw new CustomAuthenticationException(Constant.CODE_TOKEN_ERROR, "token无效,请重新登录");
        }
        if (JWTUtil.isExpired(token)){
            throw new CustomAuthenticationException(Constant.CODE_TOKEN_ERROR, "token无效,请重新登录");
        }
        return new SimpleAuthenticationInfo(token, Constant.CREDENTIALS_EMPTY, this.getName());
    }

    /**
     * 用户授权
     * 本方法被 org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo() 调用
     *     Cache<Object, AuthorizationInfo> cache = this.getAvailableAuthorizationCache();
     *     如果获取到缓存,就通过cache.get(key)获取授权数据,key就是principal
     *
     *     若无缓存,在调用了本方法后,会将本方法返回的AuthorizationInfo添加到缓存中
     *     cache.put(key, info);
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = JWTUtil.getUsername(principalCollection.getPrimaryPrincipal().toString());
        User user = userService.getUser(username);

        SimpleAuthorizationInfo sai = new SimpleAuthorizationInfo();

        List<String> roleList = new ArrayList<>();
        List<String> permissionList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(user.getRoleList())){
            for (Role role:user.getRoleList()){
                roleList.add(role.getName());
                if (!CollectionUtils.isEmpty(role.getPermissionList())){
                    for (Permission permission:role.getPermissionList()){
                        permissionList.add(permission.getName());
                    }
                }
            }
        }
        sai.addRoles(roleList);
        sai.addStringPermissions(permissionList);

        return sai;
    }
}

shiro configuration class

@Configuration
public class ShiroConfig {

    // 使用@Lazy避免UserService为空
    @Lazy
    @Autowired
    UserService userService;

    // 创建jwtRealm
    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm(userService);
    }

    @Bean
    public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 设置realm
        manager.setRealm(jwtRealm);

        /**
         * 禁止session持久化存储
         * 一定要禁止session持久化。不然清除认证缓存、授权缓存后,shiro依旧能从session中读取到认证信息
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setSecurityManager(securityManager);

        //添加filter,factoryBean.getFilters()获取到的是引用,可直接添加值
        factoryBean.getFilters().put("jwt", new JwtFilter());
        factoryBean.getFilters().put("customPerms", new CustomPermissionsAuthorizationFilter());

        /**
         * factoryBean.getFilterChainDefinitionMap();默认是size=0的LinkedHashMap
         */
        Map<String, String> definitionMap = factoryBean.getFilterChainDefinitionMap();

        /**
         * definitionMap是一个LinkedHashMap,是一个链表结构
         * put的顺序很重要,当/open/**匹配到请求url后,将不再检查后面的规则
         * 官方将这种原则称为 FIRST MATCH WINS
         * https://waylau.gitbooks.io/apache-shiro-1-2-x-reference/content/III.%20Web%20Applications/10.%20Web.html
         */
        definitionMap.put("/open/**", "anon");

        /**
         * 由于禁用了session存储,shiro不会存储用户的认证状态,所以在接口授权之前要先认证用户,不然CustomPermissionsAuthorizationFilter不知道用户是谁
         * 实际项目中可以将这些接口权限规则放到数据库中去
         */
        definitionMap.put("/user/add", "jwt, customPerms["+userService.getUserAdd().getName()+"]");
        definitionMap.put("/user/delete", "jwt, customPerms["+userService.getUserDelete().getName()+"]");
        definitionMap.put("/user/edit", "jwt, customPerms["+userService.getUserEdit().getName()+"]");
        definitionMap.put("/user/view", "jwt, customPerms["+userService.getUserView().getName()+"]");
        definitionMap.put("/test/other", "jwt, customPerms["+userService.getTestOther().getName()+"]");
        definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");

        // 前面的规则都没匹配到,最后添加一条规则,所有的接口都要经过com.example.shirojwt.filter.JwtFilter这个过滤器验证
        definitionMap.put("/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(definitionMap);

        return factoryBean;
    }
}

First ask someone to my github download the code, running on their computers.

shiro authentication, authorization processes in this project

Create a landing Interface

@RestController
@Slf4j
@RequestMapping("/open")
public class OpenApiCtrl {
    @Autowired
    UserService userService;

    /**
     * 登陆
     * 开放接口,不使用shiro拦截,生成令牌并返回给前端
     * 用户名             密码
     * admin           admin-pwd
     * cudrOtherUser   cudrOtherUser-pwd
     * viewUser        viewUser-pwd
     */
    @PostMapping("/login")
    public Result openLogin(@RequestBody User userVo){
        String username = userVo.getUsername();
        String password = userVo.getPassword();

        // 用户名密码校验
        User user = userService.getUser(username);
        if (user == null){
            throw new CustomException("无此用户");
        }
        if (!user.getPassword().equals(new Md5Hash(password).toString())){
            throw new CustomException("用户名或密码错误");
        }

        // 生成令牌
        String token = JWTUtil.sign(username, user.getPassword());

        /**
         * 在登陆接口中就执行shiro用户认证,用于测试不禁用session存储的情形
         */
        //JwtToken jwtToken = new JwtToken(token);
        //Subject subject = SecurityUtils.getSubject();
        //subject.login(jwtToken);

        return Result.success(token);
    }

}

The front end of the token to get token, then requests are put in the Authorization header = token

Then send a request

curl --location --request GET 'localhost:8080/user/add' \
--header 'Authorization: XXXXX'

This request through the two filter, JwtFilter and CustomPermissionsAuthorizationFilter, performs user authentication and authorization.

Certification process is this:

. 1, AccessControlFilter # onPreHandle ()
2, AuthenticationFilter isAccessAllowed # (), this method only determines whether the current subject (subject user) has been authenticated, the result subject.authenticated unauthenticated.
3, JwtFilter # onAccessDenied (), the user is not authenticated, this method is called.
4, JwtFilter # executeLogin (), using the Authorization request header generation JwtToken, then perform subject.login (JwtToken);
. 5, # AuthenticatingRealm getAuthenticationInfo (), by JwtToken query cache AuthenticationInfo
. 6, no cache AuthenticationInfo, enter JwtRealm # doGetAuthenticationInfo (), the legitimacy check token and generate SimpleAuthenticationInfo.

7, DefaultWebSubjectFactory # createSubject, use SimpleAuthenticationInfo the principals generate a Verified subject, user authentication is complete.

Authorization process is this:

. 1, AccessControlFilter # onPreHandle ()
2, PermissionsAuthorizationFilter isAccessAllowed # (), determining whether the subject includes an interface permission.
3, AuthorizingRealm # getAuthorizationInfo (), to obtain authorization cache.
4, no authorization cache to obtain authorization information JwtRealm # doGetAuthorizationInfo ().

Test case of the use of the session. Preparatory steps are as follows:

1, the OpenApiCtrl # openLogin () in the annotated code release.

JwtToken jwtToken = new JwtToken(token);
Subject subject = SecurityUtils.getSubject();
subject.login(jwtToken);

2, ShiroConfig # securityManager () commented disabling code stored session.

//DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
//DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
//defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
//subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
//manager.setSubjectDAO(subjectDAO);

3, ShiroConfig # shiroFilterFactoryBean (), permission to use the interface only CustomPermissionsAuthorizationFilter

definitionMap.put("/user/add", "customPerms["+userService.getUserAdd().getName()+"]");
definitionMap.put("/user/delete", "customPerms["+userService.getUserDelete().getName()+"]");
definitionMap.put("/user/edit", "customPerms["+userService.getUserEdit().getName()+"]");
definitionMap.put("/user/view", "customPerms["+userService.getUserView().getName()+"]");
definitionMap.put("/test/other", "customPerms["+userService.getTestOther().getName()+"]");
definitionMap.put("/role/permission/edit", "customPerms["+userService.getRolePermisssionEdit().getName()+"]");

Start Project

1, using the first request postman localhost: 8080 / open / login

2, then use the postman request localhost: 8080 / user / view, but the header without Authorization, you will find the request still succeed.

Why is this?

1, find PermissionsAuthorizationFilter # isAccessAllowed (), and there are a line of code 

Subject subject = getSubject(request, response);

We talked about, the subject is in shiro user, subject of the principals as user id, how do you know that shiro user who initiated the request is it?

2, DefaultWebSubjectFactory # createSubject () This method has the following code

PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();

We debug the source code to know, without disabling the session, shiro the principals (user identity), authenticated (authentication state) stored in the session in the.

This is why the request localhost: 8080 / user / view regardless of the time with or without reason Authorization request header can be authorized.

If the cookies, session are deleted postman in

Request again localhost: 8080 / user / view. debug source
PrincipalCollection principals = wsc.resolvePrincipals (); // principals is null
Boolean wsc.resolveAuthenticated Authenticated = (); // Authenticated is false

CustomPermissionsAuthorizationFilter # onAccessDenied () is called, the return to "insufficient privileges" information.

Regular Clear Cache

    /**
     * 测试清除jwtRealm缓存
     * MemoryConstrainedCacheManager使用的是Map作为缓存,必须用定时器清理
     */
    @GetMapping("/cache/test")
    public Result test2(){
        Cache<Object, AuthenticationInfo> authen = jwtRealm.getAuthenticationCache();
        Cache<Object, AuthorizationInfo> author = jwtRealm.getAuthorizationCache();
        log.info("当前缓存, 认证缓存 = {} 授权缓存 = {}", authen, author);

        authen.clear();
        author.clear();
        log.info("缓存清除完成,认证缓存size = {} 授权缓存size = {}", authen.size(), author.size());

        return Result.success("test");
    }

Modify permissions configuration

    /**
     * 测试动态修改接口授权配置
     */
    @GetMapping("/definition/test")
    public Result test1() throws Exception{

        AbstractShiroFilter shiroFilter = (AbstractShiroFilter)shiroFilterFactoryBean.getObject();
        PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
        DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
        // 清空老的权限控制
        manager.getFilterChains().clear();
        shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

        // 生成新的definitionMap
        LinkedHashMap<String, String> definitionMap = new LinkedHashMap<>();
        definitionMap.put("/open/**", "anon");

        definitionMap.put("/user/delete", "jwt, customPerms["+userService.getUserDelete().getName()+"]");
        definitionMap.put("/user/edit", "jwt, customPerms["+userService.getUserEdit().getName()+"]");
        definitionMap.put("/user/add", "jwt, customPerms["+userService.getUserAdd().getName()+"]");
        definitionMap.put("/user/view", "jwt, customPerms["+userService.getUserView().getName()+"]");
        definitionMap.put("/test/other", "jwt, customPerms["+userService.getTestOther().getName()+"]");
        definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");

        definitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);

        // 重新构建生成权限过滤链
        for (Map.Entry<String, String> entry : definitionMap.entrySet()) {
            String url = entry.getKey();
            String chainDefinition = entry.getValue();
            manager.createChain(url, chainDefinition);
        }

        return Result.success("test");
    }

 

Published 51 original articles · won praise 14 · views 40000 +

Guess you like

Origin blog.csdn.net/u010606397/article/details/104110093