zoukankan      html  css  js  c++  java
  • springboot+shiro+jwt实现登录

    前些日子我曾经使用shiro来实现用户的登录,将账号密码托管给shiro,客户端与服务端的连接通过cookie和session,

    但是目前使用最多的登录都是无状态的,使用jwt或者oauth来实现登录,所以也特地记录一下。

    1.第一步先添加jwt的依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
             <version>3.7.0</version>
        </dependency>

    2.修改shiro的配置,大体上没有什么大的变化,主要就是关闭session和配置jwt到shiro中

      @Bean
        public MyShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
            MyShiroRealm myShiroRealm= new MyShiroRealm();
            myShiroRealm.setCredentialsMatcher(matcher);
            return myShiroRealm;
        }
        @Bean
        public DefaultWebSecurityManager securityManager(HashedCredentialsMatcher matcher){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myShiroRealm(matcher));
            /*
             * 关闭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;
        }
    
        //如果没有此name,将会找不到shiroFilter的Bean
        @Bean(name = "shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            //shiroFilterFactoryBean.setLoginUrl("/login");         //表示指定登录页面 (前后分离不适用)
            //shiroFilterFactoryBean.setSuccessUrl("/user/list");   // 登录成功后要跳转的链接 (前后分离不适用)
    
            Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();//拦截器, 配置不会被拦截的链接 顺序判断
            //filterChainDefinitionMap.put("/login","anon");    //所有匿名用户均可访问到Controller层的该方法下
            filterChainDefinitionMap.put("/userLogin","anon");
            filterChainDefinitionMap.put("/image/**","anon");
            filterChainDefinitionMap.put("/css/**", "anon");
            filterChainDefinitionMap.put("/fonts/**","anon");
            filterChainDefinitionMap.put("/js/**","anon");
            filterChainDefinitionMap.put("/logout","logout");
            filterChainDefinitionMap.put("/**", "authc");    //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
            //filterChainDefinitionMap.put("/**", "user");   //user表示配置记住我或认证通过可以访问的地址
    
            // 添加自己的过滤器并且取名为jwt
            LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
            filterMap.put("jwt", jwtFilter());
            shiroFilterFactoryBean.setFilters(filterMap);
            // 过滤链定义,从上向下顺序执行,一般将放在最为下边
            filterChainDefinitionMap.put("/**", "jwt");
    
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
        @Bean
        public JwtFilter jwtFilter() {
            return new JwtFilter();
        }
    
        /**
         * SpringShiroFilter首先注册到spring容器
         * 然后被包装成FilterRegistrationBean
         * 最后通过FilterRegistrationBean注册到servlet容器
         * @return
         */
        @Bean
        public FilterRegistrationBean delegatingFilterProxy(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            DelegatingFilterProxy proxy = new DelegatingFilterProxy();
            proxy.setTargetFilterLifecycle(true);
            proxy.setTargetBeanName("shiroFilter");
            filterRegistrationBean.setFilter(proxy);
            return filterRegistrationBean;
        }
    
        @Bean(name = "hashedCredentialsMatcher")
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            hashedCredentialsMatcher.setHashAlgorithmName("MD5");
            hashedCredentialsMatcher.setHashIterations(1024);// 设置加密次数
            return hashedCredentialsMatcher;
        }
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(HashedCredentialsMatcher matcher) {//@Qualifier("hashedCredentialsMatcher")
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(matcher));
            return authorizationAttributeSourceAdvisor;
        }
        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }

    3.封装token来替换Shiro原生Token,要实现AuthenticationToken接口

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

    4.添加一个JwtUtil的工具类来操作token

    public class JwtUtil {
        /**
         * 过期时间30分钟
         */
        public static final long EXPIRE_TIME = 30 * 60 * 1000;
    
        /**
         * 校验token是否正确
         * @param token  密钥
         * @param secret 用户的密码
         * @return 是否正确
         */
        public static boolean verify(String token, String username, String secret) {
            try {
                // 根据密码生成JWT效验器
                Algorithm algorithm = Algorithm.HMAC256(secret);
                JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
                // 效验TOKEN
                DecodedJWT jwt = verifier.verify(token);
                log.info(jwt+":-token is valid");
                return true;
            } catch (Exception e) {
                log.info("The token is invalid{}",e.getMessage());
                return false;
            }
        }
    
        /**
         * 获得token中的信息无需secret解密也能获得
         * @return token中包含的用户名
         */
        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;
            }
        }
    
        /**
         * 生成签名,5min(分钟)后过期
         * @param username 用户名
         * @param secret   用户的密码
         * @return 加密的token
         */
        public static String sign(String username, String secret) {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            return JWT.create()
                      .withClaim("username", username)
                      .withExpiresAt(date)
                      .sign(algorithm);
        }
    }

    5.写一个拦截器JwtFilter,继承BasicHttpAuthenticationFilter类

    @Slf4j
    public class JwtFilter extends BasicHttpAuthenticationFilter {
        @Autowired
        private RedisUtil redisUtil;
        private AntPathMatcher antPathMatcher =new AntPathMatcher();
        /**
         * 执行登录认证(判断请求头是否带上token)
         * @param request
         * @param response
         * @param mappedValue
         * @return
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            log.info("JwtFilter-->>>isAccessAllowed-Method:init()");
            //如果请求头不存在token,则可能是执行登陆操作或是游客状态访问,直接返回true
            if (isLoginAttempt(request, response)) {
                return true;
            }
            //如果存在,则进入executeLogin方法执行登入,检查token 是否正确
            try {
                executeLogin(request, response);return true;
            } catch (Exception e) {
                throw new AuthenticationException("Token失效请重新登录");
            }
        }
    
        /**
         * 判断用户是否是登入,检测headers里是否包含token字段
         */
        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
            log.info("JwtFilter-->>>isLoginAttempt-Method:init()");
            HttpServletRequest req = (HttpServletRequest) request;
            if(antPathMatcher.match("/userLogin",req.getRequestURI())){
                return true;
            }
            String token = req.getHeader(CommonConstant.ACCESS_TOKEN);
            if (token == null) {
                return false;
            }
            Object o = redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token);
            if(ObjectUtils.isEmpty(o)){
                return false;
            }
            log.info("JwtFilter-->>>isLoginAttempt-Method:返回true");
            return true;
        }
    
        /**
         * 重写AuthenticatingFilter的executeLogin方法丶执行登陆操作
         */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            log.info("JwtFilter-->>>executeLogin-Method:init()");
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);//Access-Token
            JwtToken jwtToken = new JwtToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获, 反之则代表登入成功,返回true
            getSubject(request, response).login(jwtToken);return true;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            log.info("JwtFilter-->>>preHandle-Method:init()");
            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"));
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    }

    6.修改自定义的Realm

    public class MyShiroRealm extends AuthorizingRealm {
        @Autowired
        private RoleService roleService;
        @Autowired
        private UserService userService;
        @Autowired
        private PermissionService permissionService;
        @Autowired
        private RedisUtil redisUtil;
        /**
         * 必须重写此方法,不然Shiro会报错
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    
        /**
         * 访问控制。比如某个用户是否具有某个操作的使用权限
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            User user  = (User) principalCollection.getPrimaryPrincipal();if (user == null) {
                log.error("授权失败,用户信息为空!!!");
                return null;
            }
            try {
                //获取用户角色集
                Set<String> listRole= roleService.findRoleByUsername(user.getUserName());
                simpleAuthorizationInfo.addRoles(listRole);
    
                //通过角色获取权限集
                for (String role : listRole) {
                    Set<String> permission= permissionService.findPermissionByRole(role);
                    simpleAuthorizationInfo.addStringPermissions(permission);
                }
                return simpleAuthorizationInfo;
            } catch (Exception e) {
                log.error("授权失败,请检查系统内部错误!!!", e);
            }
            return simpleAuthorizationInfo;
        }
    
        /**
         * 用户身份识别(登录")
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            String token = (String) authenticationToken.getCredentials();// 校验token有效性
            String username = JwtUtil.getUsername(token);if (Strings.isNullOrEmpty(username)) {
                throw new AuthenticationException("token非法无效!");
            }// 查询用户信息
            User sysUser = userService.selectUserOne(username);
            if (sysUser == null) {
                throw new AuthenticationException("用户不存在!");
            }// 判断用户状态
            if (sysUser.getValid()==0) {
                throw new AuthenticationException("账号已被禁用,请联系管理员!");
            }

    return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName()); } }

    7.登录接口修改

    public class LoginController {
        @Autowired
        private UserMapper userMapper;
        @Autowired
        private RedisUtil redisUtil;
    
        /**
         * 登录
         * @return
         */
        @PostMapping(value = "/userLogin")
        @ResponseBody
        public Result<JSONObject> toLogin(@RequestBody User loginUser) throws Exception {
            Result<JSONObject> result = new Result<>();
            String userName = loginUser.getUserName();
            String passWord = loginUser.getPassWord();
    
            User user=userMapper.selectUserOne(userName);
            if (user == null) {
                return result.error500("该用户不存在");
            }
            if (user.getValid()==0) {
                return result.error500("账号已被禁用,请联系管理员!");
            }
         //我的密码是使用uuid作为盐值加密的,所以这里登陆时候还需要做一次对比 SimpleHash simpleHash
    = new SimpleHash("MD5", passWord, user.getSalt(), 1024); if(!simpleHash.toHex().equals(user.getPassWord())){ return result.error500("密码不正确"); } // 生成token String token = JwtUtil.sign(userName, passWord); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token,JwtUtil.EXPIRE_TIME / 1000); JSONObject obj = new JSONObject(); obj.put("token", token); obj.put("userInfo", user); result.setResult(obj); result.success("登录成功"); return result; } }

    添加的方法,这里的加密算法和加密次数以及盐值都要一致,否则登录时候密码对比会失败

    @RequestMapping("/insertUser")
        @ResponseBody
        public int insertUser(User user){
            //将uuid设置为密码盐值
            String salt = UUID.randomUUID().toString().replaceAll("-","");
            SimpleHash simpleHash = new SimpleHash("MD5", user.getPassWord(), salt, 1024);
            user.setPassWord(simpleHash.toHex()).setValid(1).setSalt(salt).setCreateTime(new Date()).setDel(0);
            return  userMapper.insertSelective(user);
        }

    定义的常量

    public class CommonConstant {
        /**
         * 删除标志 1 未删除 0
         */
        public static final Integer DEL_FLAG_1 = 1;
    
        public static final Integer DEL_FLAG_0 = 0;
    
        public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
    
        public static final Integer SC_OK_200 = 200;
    
        /**
         * 访问权限认证未通过 510
         */
        public static final Integer SC_JEECG_NO_AUTHZ = 510;
    
        /**
         * 登录用户令牌缓存KEY前缀
         */
        public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小时
    
        public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_";
    
        /**
         * 0:一级菜单
         */
        public static final Integer MENU_TYPE_0 = 0;
    
        /**
         * 1:子菜单
         */
        public static final Integer MENU_TYPE_1 = 1;
    
        /**
         * 2:按钮权限
         */
        public static final Integer MENU_TYPE_2 = 2;
    
        /**
         * 是否用户已被冻结 1(解冻)正常 2冻结
         */
        public static final Integer USER_UNFREEZE = 1;
    
        public static final Integer USER_FREEZE = 2;
    
        /**
         * token的key
         */
        public static String ACCESS_TOKEN = "Access-Token";
    
        /**
         * 登录用户规则缓存
         */
        public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules";
    
        /**
         * 登录用户拥有角色缓存KEY前缀
         */
        public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_";
    
        /**
         * 登录用户拥有权限缓存KEY前缀
         */
        public static String LOGIN_USER_CACHERULES_PERMISSION = "loginUser_cacheRules::Permissions_";
    }

    目前只是一个shiro+jwt的简单的登录,第一次登录的时候不需要携带token,登陆之后会返回一个token,然后可以拿着这个token去访问其他接口,

    能访问证明成功,后来经过测试,JWTToken刷新生命周期这个方法有误,第一次拿错误的token访问会报错token失效,但是第二次就可以登录成功,

    所以可以去掉那个方法,有大佬可以帮忙指出改正这个方法,如果你看到,希望能够给我一些建议,感谢!!!

  • 相关阅读:
    Linux let 命令
    perl hash array 嵌套 push
    Perl CGI编程
    Perl关联数组用法集锦
    关于反射和动态代理
    SpringBoot与web开发
    Springboot与日志
    Spring Boot
    SpringBoot的自动配置原理
    Spring MVC执行流程
  • 原文地址:https://www.cnblogs.com/red-star/p/12121941.html
Copyright © 2011-2022 走看看