• 渐进式 shiro - shiro + jwt+salt (三)


    渐进式-springboot-shiro-jwt

    完整版:springboot + shiro + jwt + salt.

    放弃 Cookie ,Session ,使用 JWT 进行鉴权,完全实现无状态鉴权

    shiro 完整流程以及集成:

    1. 用户访问登录接口 /login, 用户输入登录账号和密码被封装成 UsernamePasswordToken 对象,然后调用 subject.login() 方法
    2. shiro 立即进入用户认证过程,进入执行 UserRealm doGetAuthenticationInfo()方法代码块。
    3. 用户登录成功后,登录接口返回生成得 token.
    4. 访问其他所有需要携带token得接口,此处以支付接口 /pay为例子,必须用户登录成功访问并在请求头中添加 token。
    5. 控制哪些请求需要在请求头中添加 token,哪些请求不需要 token 可以直接访问(比如/login)的方式叫做:jwt(JSON Web Token),无状态鉴权机制
    @Slf4j
    @RestController
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @PostMapping("/login")
        public AjaxResult loginUser(@RequestBody UserEntity userVo) {
            Subject subject = SecurityUtils.getSubject();
    
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
            usernamePasswordToken.setRememberMe(true);
    
            try {
                subject.login(usernamePasswordToken);
                log.info("登录成功");
            } catch (AuthenticationException ae) {
                return AjaxResult.error("账号或密码不正确");
            }
    
            UserEntity userEntity = (UserEntity) subject.getPrincipal();
            userEntity.setToken(JwtUtils.generateToken(userEntity.getUsername(),JwtUtils.secret));
    
            return AjaxResult.success(userEntity);
    
        }
    
        @GetMapping("/pay")
        public AjaxResult payWithToken() {
            return AjaxResult.success("this Uri need token");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    引入依赖

    JWT 方案有许多种,这个网站列举了所有的常用方案,在这里我们选择 java-jwt

    <dependency>
        <groupId>org.apache.shirogroupId>
        <artifactId>shiro-spring-boot-web-starterartifactId>
        <version>1.10.0version>
    dependency>
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.12.0version>
    dependency>
    
    <dependency>
        <groupId>com.auth0groupId>
        <artifactId>java-jwtartifactId>
        <version>4.2.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ShiroConfig 配置类

    对于 HTTP 请求,springboot 默认使用 Servlet 来处理,而 shiro 的过滤器正是基于 Servlet 实现,因此所有的 Http 请求,都会执行设定好的过滤器方法.

    在前面的文章中,其实我们已经使用了过滤器,使用的都是 shiro 提供的现成过滤器名称缩写:shiro 常见过滤器

    在 ShiroConfig ShiroFilterFactoryBean 中, 对过滤器进行统一的设定.代码变动位置有 2 处

    @Configuration
    public class ShiroConfig {
    
        /**
         * 默认web安全管理器
         *
         * @return {@link DefaultWebSecurityManager}
         */
            @Bean
        public SessionManager sessionManager(){
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
            // 关闭 cookie 验证
            sessionManager.setSessionIdCookieEnabled(false);
            // 关闭 session 验证
            sessionManager.setSessionValidationSchedulerEnabled(false);
            return sessionManager;
        }
    
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    
            securityManager.setRealm(userRealm());
            securityManager.setSessionManager(sessionManager());
    
            /*
             * 关闭shiro自带的session
             * 文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
             */
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
    
            return securityManager;
        }
    
        /**
         * `shiroFilter`:过滤器
         *
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // Shiro的核心安全接口,这个属性是必须的
            shiroFilterFactoryBean.setSecurityManager(securityManager);
    
            // 自定义过滤器 ================ 变动1
            Map<String, Filter> filters = new LinkedHashMap<>();
            filters.put("myFilter", new MyFilter());
            shiroFilterFactoryBean.setFilters(filters);
    
            // 定义过滤链
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 对静态资源设置匿名访问
            filterChainDefinitionMap.put("/index.html", "anon");
            filterChainDefinitionMap.put("/favicon.ico**", "anon");
            filterChainDefinitionMap.put("/static/**","anon");
    
            // 登录,不需要拦截的访问
            filterChainDefinitionMap.put("/login", "anon");
            // 错误页面无需认证
            filterChainDefinitionMap.put("/error","anon");
    
    
            // !!! 其他所有请求使用自定义的过滤器 myFilter 来处理  ================ 变动2
            filterChainDefinitionMap.put("/**","myFilter")
            // filterChainDefinitionMap.put("/**","authc");
    
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    filterChainDefinitionMap.put("/",“myFilter”)** 用来设置其他所有请求包括 /pay 会执行即将创建的自定义过滤器 myFilter

    自定义 UserRealm

    在 ShiroConfig 中,规定了只有登录接口(subjet.login())会使用 UserRealm.需要携带token得接口与UserRealm 毫无关系

    ShiroRealm 设计为可拔插模块,而 Realm 又分为两部分:认证,授权。两个单词非常相似。

    • 授权 doGetAuthorizationInfo: 处理角色是否能够访问相应的 web service 相关信息
    • 认证 doGetAuthenticationInfo: 处理角色登录相关信息
    public class UserRealm extends AuthorizingRealm {
    
        /**
         * shiro默认机制是 通过token的类型来确认是否由当前realm来处理当前收到的登录请求
         * 因此 在这里限定只有通过 UsernamePasswordToken这个类,调用的login接口可以使用此Realm认证
         * 
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof UsernamePasswordToken;
        }
    
        /**
         * 授权
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            return null;
        }
        /**
         * 认证
         * AuthenticationToken 接口提供了2方法,getPrincipal() 返回的用户的账号信息,getCredentials() 返回的是密码信息。
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            // 登陆时传入的用户名,密码
            UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;
            // 获取用户名
            String username = (String) authenticationToken.getPrincipal();
    
            // 查询用户
            UserEntity userEntity = userService.getOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, accessToken.getUsername()));
    
            // 组装并返回
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    userEntity, // 用户
                    accessToken.getPassword(), // 密码
                    ByteSource.Util.bytes(salt),// byte类型 salt
                    "anyRealmName"  // realm name .  getName()
            );
            return authenticationInfo;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    自定义过滤器 MyFilter

    当访问 /pay时,请求会执行 MyFilter 中代码块。需要注意的是,当你修改了代码后,要先执行以下 /login 登录,复制返回得 token,再访问此接口

    
    /**
     * $$ 代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin
     *
     * @author ifredomvip@gmail.com
     * @version 1.0.0
     * @date 2022/10/27 17:18
     **/
    @Slf4j
    public class MyFilter extends BasicHttpAuthenticationFilter {
        /**
         * 过滤器拦截请求的入口方法,所有请求都会进入该方法
         * 1. 返回true则允许访问
         * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
         * @param request     请求
         * @param response    响应
         * @param mappedValue 映射值
         * @return boolean
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            log.info("允许访问 - 周期");
            // 所有自定义过滤器得请求都需要携带token
            return false;
        }
    
        /**
         * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
         *
         * 所有过滤器处理请求都可以在 isAccessAllowed中处理,或者在 onAccessDenied 中处理
         *
         * 由于过滤器在controller前运行,token过期时,抛出的异常不会全局异常捕获,而在 onAccessDenied 是可以精准抛出此异常。
         *
         * 所以在 onAccessDenied 中处理
         *
         * @param request  请求
         * @param response 响应
         * @return boolean
         * @throws Exception 异常
         */
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            log.info("拒绝访问 - 周期");
            //获取请求token,如果token不存在,直接返回401
            String token = getRequestToken((HttpServletRequest) request);
    
            // 请求头不含token
            if(StringUtils.isEmpty(token)) {
                responseError(response,HttpStatus.UNAUTHORIZED.value(),"token不能为空");
                return false;
            }
            // 请求头含有 token
            String username = JwtUtils.getUserName(token);
            if(!JwtUtils.verify(token, username, JwtUtils.secret)){
                responseError(response,HttpStatus.UNAUTHORIZED.value(),"token无效");
                return false;
            }
    
            if(JwtUtils.isExpired(token)) {
                responseError(response,HttpStatus.UNAUTHORIZED.value(),"token已失效,请重新登录!");
                return false;
            }
            log.info(String.valueOf("verify"));
            return true;
        }
    
    
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            fillCorsHeader(httpRequest,httpResponse);
    
            // 过滤options方法。跨域时会首先发送一个option请求
            if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpResponse.setStatus(HttpStatus.OK.value());
                return true;
            }
            return super.preHandle(request, response);
        }
    
        /**
         * 获取请求的token
         */
        private String getRequestToken(HttpServletRequest httpRequest) {
            //从header中获取token
            String token = httpRequest.getHeader("Authorization");
    
            //如果header中不存在token,则从参数中获取token
            if (StringUtils.isBlank(token)) {
                token = httpRequest.getParameter("Authorization");
            }
    
            return token;
        }
    
        /**
         * 跨域请求的解决方案之一
         *
         * @param request  请求
         * @param response 响应
         */
        protected void fillCorsHeader(HttpServletRequest request, HttpServletResponse response) {
            response.setContentType("text/html;charset=UTF-8");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
            response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            response.setHeader(
                    "Access-Control-Allow-Headers",
                    request.getHeader("Access-Control-Request-Headers")
            );
        }
    
        protected void responseError(ServletResponse response,int code,String errorMsg) throws IOException  {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            AjaxResult r = AjaxResult.error(HttpStatus.UNAUTHORIZED.value(), "token不能为空");
    
            String json = new ObjectMapper().writeValueAsString(r);
            httpResponse.getWriter().print(json);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    JwtUtils

    工具类都定义为静态方法,使用时可以避免注入此工具类。

    package com.mock.water.core.utils;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTDecodeException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.mock.water.modules.system.user.entity.UserEntity;
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    /**
     * @Author ifredomvip@gmail.com
     * @Date 2022/11/9 11:14
     */
    @Slf4j
    public class JwtUtils {
        /**
         * 密钥
         */
        public static String secret = "ifredom123456";
        /**
         * 到期时间 7天
         */
        public static long expire = 7*1000*60*60*24;
    
        /**
         * 创建 token
         */
        public static String generateToken(String username, String secret) {
            Date now = new Date();
            Date date = new Date(now.getTime() + expire * 1000);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        }
    
        /**
         * 验证 token
         */
        public static boolean verify(String token, String username, String secret) {
            try {
                Algorithm algorithm = Algorithm.HMAC256(secret);
                JWTVerifier verifier = JWT.require(algorithm)
                        .withClaim("username", username)
                        .build();
                verifier.verify(token);
                return true;
            } catch (Exception e) {
                log.error("token 无效 {}", e.getMessage());
                return false;
            }
        }
    
        /**
         * token是否过期
         *
         * @return true:过期
         */
        public static boolean isExpired(String token) {
            DecodedJWT jwt = JWT.decode(token);
            return System.currentTimeMillis() > jwt.getExpiresAt().getTime();
        }
    
        /**
         * 从 token中获取字段
         *
         * @return token中包含的填入字段
         */
        public static String getClaim(String token, String claim) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim(claim).asString();
            } catch (JWTDecodeException e) {
                log.error("error:{}", e.getMessage());
                return null;
            }
        }
    
        public static String getUserName(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("username").asString();
            } catch (JWTDecodeException e) {
                log.error("error:{}", e.getMessage());
                return null;
            }
        }
    
        public String getSecret()  {return secret;}
        public void setSecret(String secret) {this.secret = secret;}
        public void setExpire(long expire) {this.expire = expire;}
        public long getExpire() {return expire;}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    Realm 中 SimpleAuthenticationInfo 详解

    在认证功能中会使用到 Shiro 封装好的 SimpleAuthenticationInfo 类.

    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
            userEntity, // 用户
            accessToken.getPassword(), // 密码
            ByteSource.Util.bytes(salt),// byte类型 salt
            "anyRealmName"  // realm name .  getName()
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 第一个参数,可以传入用户名 username,也可以传入从数据库查询得到的 userEntity 实体对象。(shiro 会自动调用实体的 getUserName()去获取 username字段值)建议传入 userEntity

      传入 userEntity(实体),subject.getPrincipal() 得到的是 userEntity(实体);

      传入 username(字符串),subject.getPrincipal() 得到的是字符串。

    • 第二个参数,传入的是用户登录时输入的 password。它被装入 SimpleAuthenticationInfo类返回后,会与 UsernamePasswordToken 中的 password 进行对比。匹配上了就表明验证通过,匹配不上就报异常。

      需要注意,网上很多文章说的是传入数据库中的密码,数据库中应该存放的是 username 和 salt,或者加密之后的密码,一定不能是明文密码.

    • 第三个参数(可选参数),salt 盐。此参数目的:用于对密码进行加密以及对比,防止用户的密码相同。

      具体来说就是:假如两个用户的密码都是 123456, Shiro 在比较 数据库中获取的 passwordUsernamePasswordToken 中的 password 的值时,默认会先调用这个类 new SimpleHash(String algorithmName, Object source)对密码执行一次 MD5 哈希算法得到字符串,然后使用哈希化后的两个字符串进行比较,这两字符串相同,那么就表示密码相同。(shiro 并不会上来就直接比较 2 个密码原文,会分别哈希算法转换一次后,对比转换后的值)

      所以问题就来了, 如果两个用户密码相同,在没有 salt 的情况下,他们的哈希值是一样的,就会造成错误判断。加盐后就可以避免不同用户的密码不一样。

    • 第四个参数:当前 realm 对象的 beanName, 可以通过 getName() 获取

    认证方法 doGetAuthenticationInfo 入参 AuthenticationToken 详解

    认证方法 doGetAuthenticationInfo() 有一个入参,类型为 AuthenticationToken

    protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken authenticationToken
    ) throws AuthenticationException {}
    
    • 1
    • 2
    • 3

    参数 AuthenticationToken 是一个接口,它拥有 2 个实现类和 2 个继承接口,关系如下。

    参数 authenticationToken 从哪里来呢?

    它是在登陆 login() 时,我们创建一个 UsernamePasswordToken 对象然后传入的,传入的必须是一个AuthenticationToken的实现类.(经过测试,此处并不能传入 new BearerToken()这个实现类)

    @PostMapping("/login")
    public void loginUser(@Validated @RequestBody UserVo userVo, BindingResult bindingResult) {
        Subject subject = SecurityUtils.getSubject();
    
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userVo.getUsername(), userVo.getPassword());
        usernamePasswordToken.setRememberMe(true);
    
        // 传入
        subject.login(usernamePasswordToken);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从继承关系图可以看出,为什么它可以向下转型

    UsernamePasswordToken accessToken = (UsernamePasswordToken) authenticationToken;
    
    • 1

    过滤链 ShiroFilterChainDefinition

    在配置类中,shiro 提供了一个简单得封装类 ShiroFilterChainDefinition,可以将过滤连提取为一个单独得方法,代码看上去更为舒适.

    @Configuration
    public class ShiroConfig {
    
        ......
    
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    
            // 自定义过滤器
            Map<String, Filter> filters = new LinkedHashMap<>();
            filters.put("myFilter", new MyFilter());
            shiroFilterFactoryBean.setFilters(filters);
    
            // 定义过滤链
            Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
    
            return shiroFilterFactoryBean;
        }
    
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    
            // 不需要拦截的访问
            // 对静态资源设置匿名访问
            chainDefinition.addPathDefinition("/index.jsp", "anon");
            chainDefinition.addPathDefinition("/login.jsp", "anon");
            chainDefinition.addPathDefinition("/favicon.ico**", "anon");
            chainDefinition.addPathDefinition("/captcha/captchaImage**", "anon");
            chainDefinition.addPathDefinition("/static/**","anon");
    
            // 登录,
            chainDefinition.addPathDefinition("/login", "anon");
            // 注册
            chainDefinition.addPathDefinition("/register", "anon");
            // 错误页面
            chainDefinition.addPathDefinition("/error","anon");
            // 登出,shiro 自动清除 session
            chainDefinition.addPathDefinition("/logout","logout");
            // druid连接池的角色控制,只有拥有admin角色的admin用户可以访问,不理解可以先不管
            chainDefinition.addPathDefinition("/druid/**","authc, roles[admin]");
    
            // 其余资源都交给 MyFilter 这个过滤器处理
            chainDefinition.addPathDefinition("/**","myFilter");
            return chainDefinition;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    DefaultShiroFilterChainDefinition内部使用 LinkedHashMap 实现 . HashMap 是无序的,LinkedHashMap 将会按序加载。最后添加了 chainDefinition.addPathDefinition(“/**”, “authc”), 如果使用 HashMap 将会优先加载了此配置,导致其他配置失效。

    常见过滤器

    配置缩写对应的过滤器功能
    anonAnonymousFilter指定 url 可以匿名访问
    authcFormAuthenticationFilter基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure)

    AuthorizationInfo 授权

    shiro 对权限授权划分为:角色 和 资源

    • 角色

    • 资源权限
      ————————————————
      注解方式授权

    • @RequiresAuthentication: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 必须在当前 session 中已经过认证。

    • @RequiresGuest: 使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 可以是“gust”身份,不需要经过认证或者在原先的 session 中存在记录。

    • @RequiresPermissions: 当前 Subject 需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject 不具有这样的权限,则方法不会被执行。

    • @RequiresRoles: 当前 Subject 必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject 不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException 异常。

    • @RequiresUser:当前 Subject 必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

      实例

    issuses

    1. ShiroFilterFactoryBean 方法名取名为 shiroFilterFactoryBean()
    2. logout功能。如果再拦截器链中配置了 logout,那么不要再定义 controller。只定义其中一个
    @Configuration
    public class ShiroConfig {
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    
            chainDefinition.addPathDefinition("/logout","logout");
    
            return chainDefinition;
        }
    }
    
    ====================
    
    @RestController
    public class UserController {
        @GetMapping("/logout")
        public void logout(){
            SecurityUtils.getSubject().logout();
            System.out.println("登出");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.shiro 中出现 does not support authentication token

    /**
        * 大坑!,必须重写此方法,不然Shiro会报错
        */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. Filter 中的依赖注入.在配置的过程中, 一个坑点就是: 如果你希望将你的 JWTUtil 工具类(或许其它)通过依赖注入的方式注入到你的自定义 filter 中, 绝对不能使用 Autowired 注解, 因为 filter 的初始化早于 beans 的初始化, 因此是无法将 bean 通过 autowired 注入到 filter 类中的. 解决方法是: 通过为 filter 类增添构造函数, 在构造函数中传入 ApplicationContext, 然后在通过 context 获取 bean.
    public class Myfilter{
    
        private JwtUtils jwtUtils;
        public LoginFilter(ApplicationContext context) {
            this.util = context.getBean(MyJWTUtil.class);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    changelog

    • 2021 年发布 shiro1.8 带来了质的飞跃,对于本文的需求来说,最利好的包括两点
    • 一是增加了对 SpringBoot 自动装配机制的支持;
    • 二是增加了 BearerHttpAuthenticationFilter 这个默认过滤器,从而让 Jwt 的整合获得了原生级的适配性。以上两项特性大大精简了我们的配置工作,且让当前网络上所有的教程都落后于时代。(包括官网和英文网络,搜到的教程基本都是旧版本的配置。)

    阅读资料

  • 相关阅读:
    webpack优化
    【C++】不用include,使用C++模块语法实现函数调用
    GIS教程之创建 GIS 数据的 web 地图最简单的方法streamlit、ellipsis 和 folium
    CentOS7安装flink1.17完全分布式
    【优化算法】基于matlab量子粒子群算法求解单目标优化问题【含Matlab源码 2203期】
    基于B/S的旅游景点网站设计(Java+Web+MySQL)
    怎样实现纯前端百万行数据秒级响应
    「运维有小邓」企业/员工目录搜索
    在阿里晋升3次,4年拿下P8岗位,这份pdf或许对你有帮助
    containerd 镜像构建工具 -- nerdctl 和 buildkit
  • 原文地址:https://blog.csdn.net/win7583362/article/details/127899311