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("账号已被禁用,请联系管理员!");
        }// 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
            throw new AuthenticationException("Token失效请重新登录!");
        }return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName());
    }

    /**
     * JWTToken刷新生命周期 (解决用户一直在线操作,提供Token失效问题)
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWTToken值还在生命周期内,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
     * 4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
     * 5、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
     * 6、每次当返回为true情况下,都会给Response的Header中设置Authorization,该Authorization映射的v为cache对应的v值。
     * 7、注:当前端接收到Response的Header中的Authorization值会存储起来,作为以后请求token使用
     * 参考方案:https://blog.csdn.net/qq394829044/article/details/82763936
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        log.info("jwtTokenRefresh参数:token="+token+",userName="+userName+",passWord="+passWord);
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));if (!Strings.isNullOrEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.sign(userName, passWord);
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization,JwtUtil.EXPIRE_TIME / 1000);
            } else {
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken,JwtUtil.EXPIRE_TIME / 1000);
            }
            return true;
        }
        return false;
    }
}

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去访问其他接口,

能访问证明成功,后面关于jwt的知识会继续记录,如果你看到,希望能够给我一些建议,作为一个菜鸟,会很感谢你!!!