zoukankan      html  css  js  c++  java
  • Shiro&Jwt验证

    此篇基于 SpringBoot 整合 Shiro & Jwt 进行鉴权 相关代码编写与解析

    首先我们创建 JwtFilter 类 继承自 BasicHttpAuthenticationFilter

    org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

    此类是个过滤器,后期配置Shiro时会引用到

    重写4个重要的方法 其执行顺序亦是如下

    1. preHandle(..)  我理解为是前置处理
    2. isAccessAllowed(..) 请求是否被允许
    3. isLoginAttempt(..) 是否是尝试登陆,去查实[请求头]里是否包含[Authorization]
    4. executeLogin(..) 执行登陆操作 其会调用getSubject(request, response).login(jwtToken)进行登陆验权
    

    preHandle 方法

    可以理解为前置处理,我们在这进行一些跨域的必要设置

    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
    httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
    httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    //跨域请求会发送两次请求首次为预检请求,其请求方法为 OPTIONS
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
         httpServletResponse.setStatus(HttpStatus.OK.value());
         return false;
    }
    return super.preHandle(request, response);
    

    isAccessAllowed 方法

    其实这个方法我们会手动调用 isLoginAttempt 方法及 executeLogin 方法
    isLoginAttempt判断用户是否想尝试登陆,判断依据为请求头中是否包含 Authorization 授权信息,也就是 Token 令牌
    如果有则再执行executeLogin方法进行登陆验证操作,就是我们整合后的鉴权操作,因为用Token抛开了Session,此处就相当于是否存在Session的操作,存在则表明登陆成功,不存在则需要登陆操作,或者Session过期需要重新登陆是一个原理性质,此方法在这里是验证JwtToken是否合法,不合法则返回401需要重新登陆

    不合法的原因大致一这些

    • Token不正确 可以是被篡改 不能解析
    • Token过期
    • Token已被注销(需自己去实现)
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
            if (this.isLoginAttempt(request, response)) {
    
                // 进行验证登陆JWT 可以trycatch下可以捕获Token过期异常等信息
                this.executeLogin(request, response);
                
            } else {
                // 没有携带Token
                HttpServletRequest httpRequest = (HttpServletRequest)request;
                String httpMethod = httpRequest.getMethod();
                String requestURI = httpRequest.getRequestURI();
                logger.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                    httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                    httpServletResponse.setCharacterEncoding("UTF-8");
                    httpServletResponse.setContentType("application/json; charset=utf-8");
                    try (PrintWriter out = httpServletResponse.getWriter()) {
    
                        out.append("用户认证失败" + msg);
                    } catch (IOException e) {
                        //logger.error("直接返回Response信息出现IOException异常", e);
                    }
                return false;
            }
            return true;
        }
    

    isLoginAttempt 方法 是否尝试登陆

        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
            String token = this.getAuthzHeader(request);
            return token != null;
        }
    

    executeLogin 方法

    执行登陆操作 其实就是对 Token 进行验证操作,这里我们需要另外一个类去处理 Token 验证 (MyRealm 类的doGetAuthenticationInfo方法)

        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) {
            String token = this.getAuthzHeader(request);
            //这里需要自己实现对Token验证操作
            JwtToken jwtToken = new JwtToken(token);
            getSubject(request, response).login(jwtToken);//如果登陆失败会抛出异常(Token鉴权失败)
            return true;
        }
    

    创建 JwtToken 类 继承自AuthenticationToken

    org.apache.shiro.authc.AuthenticationToken

    这里本来是存取用户名及密码的字段用来登陆,现在因为是Token不存在需要携带用户名 所以把字段都设置成Token

    public class JwtToken implements AuthenticationToken {
        private static final long serialVersionUID = -634556778977L;
        
        private String token;
    
        public JwtToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    

    JwtConfig类的创建

    这个类用于创建 Token 及解码 Token 里的信息

    @ConfigurationProperties(prefix = "config.jwt")
    @Component
    public class JwtConfig {
    
        private String secret;
        private long expire;
        private String header;
    
        /**
         * 生成token
         *
         * @param subject
         * @return
         */
        public String createToken(String subject) {
            Date nowDate = new Date();
            //过期时间 默认单位 (天)
            Date expireDate = new Date(nowDate.getTime() + expire * 1000 * 60 * 60 * 24);
    
            return Jwts.builder()
                    .setHeaderParam("typ", "JWT")
                    .setSubject(subject)
                    .setIssuedAt(nowDate)
                    .setExpiration(expireDate)
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        /**
         * 获取token中注册信息
         *
         * @param token
         * @return
         */
        public Claims getTokenClaim(String token) {
            try {
                return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
            } catch (Exception e) {
                return null;
            }
        }
    
        /**
         * 获取token 解析信息
         * @param token
         * @return
         */
        public String getTokenInfo(String token){
            Claims claims=getTokenClaim(token);
            if(claims==null){
                throw new RuntimeException("Token 信息异常");
            }
            String tokenInfo=claims.getSubject();
            if(StringUtils.isBlank(tokenInfo)){
                throw new RuntimeException("Token 信息异常 解析值为空");
            }
            return tokenInfo;
        }
    
        /**
         * 验证token是否过期失效
         *
         * @param expirationTime
         * @return
         */
        public boolean isTokenExpired(Date expirationTime) {
            return expirationTime.before(new Date());
        }
    
        /**
         * 获取token失效时间
         *
         * @param token
         * @return
         */
        public Date getExpirationDateFromToken(String token) {
            return getTokenClaim(token).getExpiration();
        }
    
        /**
         * 获取用户名从token中
         */
        public String getUsernameFromToken(String token) {
            return getTokenClaim(token).getSubject();
        }
    
        /**
         * 获取jwt发布时间
         */
        public Date getIssuedAtDateFromToken(String token) {
            return getTokenClaim(token).getIssuedAt();
        }
    
    
        ...set get
    }
    
    

    配置文件信息

    config.jwt.secret=123!@#
    config.jwt.expire=15
    config.jwt.header=Authorization
    

    实现自己的Realm 创建MyRealm类 继承自AuthorizingRealm

    import io.jsonwebtoken.Claims;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    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.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    
    /**
     * @author zy
     */
    @Component
    public class MyRealm extends AuthorizingRealm {
    
        private static Logger logger = LogManager.getLogger(MyRealm.class);
    
        /**
        *   这里需要实现自己的用户登陆验证信息及 数据权限相关的信息获取
        */
        @Resource
        private UserService userService;
    
        @Resource
        private JwtConfig jwtConfig;
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    
        /**
         * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            logger.info("====================数据权限认证====================");
            String username = jwtConfig.getTokenInfo(principals.toString());
            UserInfo user = userService.getUserAndRole(username);
    
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    
            List<Role> roles = user.getRoles();
    
            if (roles.isEmpty()) {
                logger.warn("该用户 {} 没有角色,默认赋予user角色", username);
                Role r = new Role();
                r.setRole("user");
                roles.add(r);
            }
    
            /**
            *    这里是我自己实现的数据权限认证可做参考用
            */
            Set<String> permissionSet = new HashSet<>(roles.size() * 16);
            for (Role role : roles) {
                List<Permission> temList = role.getPermissions();
                if (temList == null || temList.isEmpty()) {
                    logger.warn("该角色 {} 没有赋予相应权限信息", role.getRole());
                    continue;
                }
                for (Permission tem : temList) {
                    if (tem.getId().indexOf(":") > -1) {
                        permissionSet.add(tem.getId());
                    }
                }
            }
    
            simpleAuthorizationInfo.setRoles(roles.stream().map(Role::getRole).collect(Collectors.toSet()));
            simpleAuthorizationInfo.setStringPermissions(permissionSet);
            return simpleAuthorizationInfo;
        }
    
        /**
         * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) {
            logger.info("====================Token认证====================");
            String token = auth.getCredentials().toString();
            Claims claims = jwtConfig.getTokenClaim(token);
            if (claims == null) {
                throw new AuthenticationException("解析Token异常");
            }
            if (jwtConfig.isTokenExpired(claims.getExpiration())) {
                throw new AuthenticationException("Token过期");
            }
            String username = claims.getSubject();
            if (username == null || username == "") {
                logger.error("Token中帐号为空");
                throw new AuthenticationException("Token中帐号为空");
            }
            UserInfo user = userService.getUserByName(username);
            if (user == null) {
                throw new AuthenticationException("该帐号不存在");
            }
            DataContextHolder.setCurrentUser(user);
            return new SimpleAuthenticationInfo(token, token, getName());
        }
    }
    

    配置Shiro 创建ShiroConfig类

    LifecycleBeanPostProcessor这个类并不一定要手动创建,手动创建可能存在一些问题。我遇见的坑就在这里。至于原因希望大家不吝赐教

    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.filter.authc.AnonymousFilter;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.RedisTemplate;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author zy
     */
    @Configuration
    public class ShiroConfig {
    
        @Bean("securityManager")
        public DefaultWebSecurityManager getManager(MyRealm myRealm) {
            DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
            manager.setRealm(myRealm);
    
            // 关闭Shiro自带的session
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            manager.setSubjectDAO(subjectDAO);
    
            // 设置自定义Cache缓存 根据项目情况而设置
            manager.setCacheManager(new CustomCacheManager());
            return manager;
        }
    
        /**
         * 添加自己的过滤器,自定义url规则
         */
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, RedisTemplate<String, String> redisTemplate, JwtConfig jwtConfig) {
            ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
    
            //配置过滤器 对 『anon』不进行拦截
            Map<String, Filter> filterMap = new HashMap<>(3);
            filterMap.put("anon", new AnonymousFilter());
            filterMap.put("jwt", new JwtFilter(redisTemplate, jwtConfig));
    
            factoryBean.setFilters(filterMap);
            factoryBean.setSecurityManager(securityManager);
    
    
            factoryBean.setLoginUrl("/login");
    
            LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(16);
    
            // 配置不过滤
            filterChainDefinitionMap.put("/login", "anon");
            filterChainDefinitionMap.put("/testpage", "anon");
            filterChainDefinitionMap.put("/static/**", "anon");
            filterChainDefinitionMap.put("/login/**", "anon");
            filterChainDefinitionMap.put("/unauthorized/**", "anon");
            // swagger
            filterChainDefinitionMap.put("/swagger**/**", "anon");
            filterChainDefinitionMap.put("/v2/**", "anon");
            filterChainDefinitionMap.put("/webjars/**", "anon");
    
            filterChainDefinitionMap.put("/**", "jwt");
    
            factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return factoryBean;
        }
    
    //    @Bean
    //    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    //        return new LifecycleBeanPostProcessor();
    //    }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    }
    
  • 相关阅读:
    打包spring项目遇到的坑 Unable to locate Spring NamespaceHandler for XML schema ……shcema/context 产生的原因及解决方法
    Mybatis 从入门到精通一:mybatis的入门
    IO流系列一:输入输出流的转换
    本地模拟 gitlab ci 的 demo 项目
    docker 容器中部署 Go 服务时,请求 https 文件时抛错
    微信支付『支付失败,如果已扣款,资金在0-3个工作日原路返回』踩坑案例及解决方案
    PHP 安装 扩展时 抛出 /usr/local/Cellar/php@7.1/7.1.25/pecl 异常解决
    SpringBoot2 引入 Aop
    Mac 下 IDEA 中 SpringBoot 如何利用 DevTool 开启热部署
    MySql数据库中敏感字段加密处理方案
  • 原文地址:https://www.cnblogs.com/dadiwm321/p/shiro_jwt.html
Copyright © 2011-2022 走看看