整体思路主要是利用shiro的鉴权机制,自定义鉴权的方法:

1、登录接口,验证登录信息后,通过JWTUtil生成token,通过JWTtoken对象(实现AuthenticationToken中接口)存入subject中

2、接口拦截逻辑,通过shiroConfig的shiroFilter确定匹配规则,在匹配规则上匹配访问的路径需要走自定义的JwtFilter(关键代码filterChainDefinitionMap.put("/**", "jwt");)

3、自定义的JwtFiler主要是为了获取token并交由realm进行验证

4、通过自定义的CustomRealm进行token的解码验证

这里涉及到三点特殊的说明:

1、因为jwt体系禁用了shiro的session体系,所以注销不能通过subject.logout()的方式进行注销,而jwt本身的token值是不能手动销毁的,这里提供一个解决的思路:注销时建立token黑名单,token正式解码验证前先进行黑名单验证。具体代码属于业务代码就不在本篇文章罗列了。

2、验证失败时使用自定义异常的方式进行处理,当系统发生异常时会自动直接返回前端异常信息,相关代码会罗列。

3、权限认证逻辑跟shiro的权限认证逻辑一致(见我的另外一篇《springboot整合shiro完成基本的登录验证》)

应用依赖

<!--shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

        <!--引入JWT依赖,由于是基于Java,所以需要的是java-jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

1、登录接口

package com.example.xxljobdemo.controller;

import com.example.xxljobdemo.util.JwtUtill;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.SecurityUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class LoginController {

    /**
     * 登陆
     *
     * @param username 用户名
     * @param password 密码
     */
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(String username, String password) {

        //username、password验证代码略。。。

        String token= JwtUtill.sign(username,password);
        SecurityUtils.getSubject().login(new JwtToken(token));
        return token;
    }
}

引用JwtUtill

package com.example.xxljobdemo.util;

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 java.util.Date;
import java.util.UUID;

public class JwtUtill {

    private static final long EXPIRE_TIME = 60 * 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);
            return true;
        } catch (Exception exception) {
            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) {
            return null;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static Integer getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asInt();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 获得tokenId
     *
     * @return uuid
     */
    public static String getTokenId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getId();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 获取token过期时间
     *
     * @return 过期时间
     */
    public static Date getExpiresAt(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getExpiresAt();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 获取token签发时间
     *
     * @return 签发时间
     */
    public static Date getIssuedAt(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getIssuedAt();
        } catch (JWTDecodeException e) {
            return null;
        }
    }


    /**
     * 生成签名
     *
     * @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);
        String jwtId = UUID.randomUUID().toString();
        // 附带username信息
        return JWT.create()
                .withJWTId(jwtId)
                .withClaim("username", username)
                .withExpiresAt(date)
                .withIssuedAt(new Date())
                .sign(algorithm);
    }

    public static void main(String[] args) {
        String token = sign("aaa", "123456");
        System.out.println("token" + token);
        System.out.println(getTokenId(token));
        System.out.println(getUserId(token));
        System.out.println(getUsername(token));
        System.out.println(getIssuedAt(token));
        System.out.println(getExpiresAt(token));
        System.out.println(verify(token, "aaa", "123456"));
    }

}

引用的JwtToken

package com.example.xxljobdemo.vo;

import org.apache.shiro.authc.AuthenticationToken;


public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

2、ShiroConfig,配置securityManager和shiroFilter规则

package com.example.xxljobdemo.config;

import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();

        //session管理
//        webSecurityManager.setSessionManager(sessionManager());

        //realm管理
        webSecurityManager.setRealm(realm());

        //缓存管理
        webSecurityManager.setCacheManager(new MemoryConstrainedCacheManager());
        //使用ehcache
//        EhCacheManager ehCacheManager = new EhCacheManager();
//        ehCacheManager.setCacheManager(getEhCacheManager());
//        webSecurityManager.setCacheManager(ehCacheManager);

        //redis实现
//        webSecurityManager.setCacheManager(redisCacheManager());

        //关闭session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        webSecurityManager.setSubjectDAO(subjectDAO);

        return webSecurityManager;
    }

//    @Bean
//    public RedisCacheManager redisCacheManager() {
//
//        RedisManager redisManager = new RedisManager();
//        redisManager.setHost("localhost:6379");
//        redisManager.setDatabase(1);
//        redisManager.setTimeout(5000);
        redisManager.setPassword();
//
//        RedisCacheManager redisCacheManager = new RedisCacheManager();
//        redisCacheManager.setRedisManager(redisManager);
//        return redisCacheManager;
//    }
//
//    @Bean
//    public CacheManager getEhCacheManager() {
//        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
//        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("classpath:org/apache/shiro/cache/ehcache/ehcache.xml"));
//        return ehCacheManagerFactoryBean.getObject();
//    }

    @Bean
    public Realm realm() {
        CustomRealm shiroRealm = new CustomRealm();
        return shiroRealm;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //开放登陆接口
        filterChainDefinitionMap.put("/login", "anon");

        //开放注销接口
        filterChainDefinitionMap.put("/logout", "anon");

        //user权限
        filterChainDefinitionMap.put("/user/**", "roles[user]");

        //admin权限
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");

        //其余接口一律拦截,走自定义拦截器jwt
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        HashMap<String, Filter> myFIleter = new HashMap<>();
        myFIleter.put("jwt", new JWTFilter());

        shiroFilterFactoryBean.setFilters(myFIleter);
        return shiroFilterFactoryBean;
    }

}

3、自定义的过滤器JWTFilter

package com.example.xxljobdemo.config;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;


public class JWTFilter extends BasicHttpAuthenticationFilter {


    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)  {
        String token = ((HttpServletRequest) request).getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new MyprojectException("token不能为空");
        }
        executeLogin(request, response);
        return true;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response)  {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new MyprojectException("token不能为空");
        }
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("登录失败");
        return super.onAccessDenied(request, response);
    }

}

4、自定义CustomRealm

package com.example.xxljobdemo.config;

import com.example.xxljobdemo.util.JwtUtill;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.authc.*;
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 java.util.HashSet;
import java.util.Set;

public class CustomRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    /**
     * 获取身份验证信息
     * Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
     *
     * @param authenticationToken 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("————身份认证方法————");
        String tokenStr=(String) authenticationToken.getPrincipal();
        String username = JwtUtill.getUsername(tokenStr);
        System.out.println("登录的用户:"+username);
        // 模拟通过用户名admin从数据库获取密码123
        String password ="";
        if("admin".equals(username)){
             password ="123";
        }
        if(JwtUtill.verify(tokenStr,username,password)){
            System.out.println("登录成功");
        }else {
            throw new UnknownAccountException("用户名密码不正确");
        }
        return new SimpleAuthenticationInfo(tokenStr, tokenStr, getName());
    }

    /**
     * 获取授权信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("————权限认证————");
        String username = JwtUtill.getUsername(principalCollection.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 模拟通过用户名admin从数据库获取权限admin
        String role ="";
        if("admin".equals(username)){
            role ="admin";
        }
        Set<String> set = new HashSet<>();
        //需要将 role 封装到 Set 作为 info.setRoles() 的参数
        set.add(role);
        //设置该用户拥有的角色
        info.setRoles(set);
        return info;
    }

}

5、自定义异常MyprojectException和统一异常捕捉返回的controller

package com.example.xxljobdemo.config;


/**
 * @author
 * @Description: 自定义业务异常类
 * @date
 */
public class MyprojectException extends RuntimeException {
    /**
     * 错误编码
     */
    private String code;
    public MyprojectException() {
        super();
    }
    public MyprojectException(String message) {
        super(message);
    }
    public MyprojectException(String code, String message) {
        super(message);
        this.code = code;
    }
    public MyprojectException(Throwable cause) {
        super(cause);
    }
    public MyprojectException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyprojectException(String message, Throwable cause,
                                boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    @Override
    public String getMessage() {
        return super.getMessage();
    }
    @Override
    public String toString() {
        return this.code + ":" + this.getMessage();
    }
}
package com.example.xxljobdemo.controller;

import com.example.xxljobdemo.config.MyprojectException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;


@RestController
@ControllerAdvice
public class ExceptionAdvice {


    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public String handleException(RuntimeException e) {
        return "运行异常:"+e.getMessage();
    }

    @ExceptionHandler(MyprojectException.class)
    @ResponseBody
    public String doBusinessException(Exception e) {
        return "运行异常:"+e.getMessage();
    }

}