SpringBoot + Shiro + Jwt 实现登录认证,代码分析

前言

花了几天了解Shiro框架(也不算太深入),根据网上资料做了一个Demo:SpringBoot 2.2.9.RELEASE+ Shiro + Jwt 实现登录认证。

1、这个Demo关注 登录授权(比较通用),基本不涉及权限控制,因为权限控制设计到具体业务。所以可以本Demo可以根据自身实际情况稍加修改,就可以作为前后端分离项目的登录模块。

2、本博客适用于对Shiro和Jwt有了解,起码对于几个关键的类的作用及方法有了解,下面我不会展开,只会对Demo的思路和代码做分析。

思路分析

1、Shiro原本的登录认证的流程

subject.login(new UsernamePasswordToken(username, password)); 这是执行登录认证的关键代码,然后Shiro内部会在对应的Realm里面的doGetAuthenticationInfo()中根据username从数据库中查找正确的User信息(包括密码信息,密码可以被加密存储)。然后将正确的User信息封装成AuthenticationInfo对象。在比较器(CredentialsMatcher)的doCredentialsMatch()方法中按照一定规则进行密码匹配(比较)。如果匹配成功,就登录成功。

登录成功后,subject的登录信息会被存储session中。那么下次访问受限的资源或接口,便不需要再登陆了。

2、加入Jwt后的登录认证的流程

Jwt和Shiro原本的登录认证有冲突吗? 其实Jwt本质上就是一种特殊的token而已。换言之,Shiro + Jwt的意思就是使用token(Jwt)替换到Shiro原本的session,使用token的服务端更加适用于前后端分离的项目。不管前端是vue项目还是Android APP等等。

替换之后的流程是什么样的? 首先,第一次登录,我们需要用户输入username和password(根据实际情况,此处简单举例),然后按照Shiro原本的登录认证流程登录。如果登录成功,服务端返回一个Jwt字符串(相当于签发一个令牌)给前端。下次用户访问服务端受限的资源,只要携带正确合法的Jwt就可以访问了。

实现的主要思路是怎样的?我们需要实现一个过滤器,拦截所有请求,对于请求头中含有Jwt的请求进行特殊处理,我们不使用Shiro默认登录流程来处理,而是使用我们自定义的处理流程,包括实现对应的Token,Realm,CredentialsMatcher。当认证通过之后,将请求放行到Controller层。

 经过一顿分析,本Demo主要划分3个部分:注册密码登录携带令牌(jwt)访问。其中,“携带令牌(jwt)访问”这个部分就是Shiro整合Jwt的核心逻辑。

核心代码分析

一、依赖引入和数据库表

数据库相关等其他依赖我就不贴出来了,详情参照Demo的Github源码:https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt

        <!-- apache为SpringBoot整合shiro提供的starter -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

        <!-- Shiro默认的缓存管理 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.5.3</version>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.3.0</version>
        </dependency>

二、注册

注册的时候,我们需要将用户输入的明文密码通过Md5Hash加密处理,传入加密的随机盐(每个用户不一样,需要存到用户表)和哈希散列的次数(每个用户都一样)。

那么在使用密码登录时,我们需要以相同的加密规则加密用户输入的密码,然后与数据库中存储的注册时的加密密码,进行比对。如果一样,登录成功。

UserController

/**
 * 通过注解实现权限控制
 * 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
 *
 * @author passerbyYSQ
 * @create 2020-08-20 23:54
 */
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 用户注册
     * @param user
     * @return
     */
    @PostMapping("/register")
    public ResponseEntity<String> register(User user) {
        // 参数判断省略
        // ...

        try {
            userService.register(user);
            return ResponseEntity.ok().build();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 错误提示信息省略...
        return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("客户端传参错误");
    }

}

UserServiceImpl

/**
 * @author passerbyYSQ
 * @create 2020-08-21 11:02
 */
@Service("userService") 
@Transactional // 开启事务。有需要再开启
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    public void register(User user) {
        // 8个字符的随机字符串,作为加密的随机盐
        String salt = RandomUtil.generateStr(8);
        // 需要保存到数据库,第一次登录(认证)比较时需要使用
        user.setSalt(salt);

        // Md5Hash默认将随机盐拼接到源字符串的前面,然后使用md5加密,再经过x次的哈希散列
        // 第三个参数(hashIterations):哈希散列的次数
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), user.getSalt(), 1024);
        user.setPassword(md5Hash.toHex());

        // 保存
        userDAO.save(user);
    }
}

三、密码登录

UserController

/**
 * 通过注解实现权限控制
 * 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
 *
 *
 * @author passerbyYSQ
 * @create 2020-08-20 23:54
 */
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;


    /**
     * 用户登录(身份认证)
     * Shiro会缓存认证信息
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    public ResponseEntity<String> login(String username, String password) {
        // 前期的注入工作已经由SpringBoot完成了
        // 获取当前来访用户的主体对象
        Subject subject = SecurityUtils.getSubject();

        try {
            // 执行登录,如果登录失败会直接抛出异常,并进入对应的catch
            subject.login(new UsernamePasswordToken(username, password));

            // 获取主体的身份信息
            // 实际上是User。为什么?
            // 取决于LoginRealm中的doGetAuthenticationInfo()方法中SimpleAuthenticationInfo构造函数的第一个参数
            User user = (User) subject.getPrincipal();

            // 生成jwt
            String jwt = userService.generateJwt(user.getUsername());

            // 将jwt放入到响应头中
            return ResponseEntity.ok().header("token", jwt).build();

        } catch (UnknownAccountException e) {
            // username 错误
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("username不存在");
        } catch (IncorrectCredentialsException e) {
            // password 错误
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("password错误");
        }
    }

    /**
     * 退出登录
     * 销毁主体的认证记录(信息),下次访问需要重新认证
     *
     * @return
     */
    @RequestMapping("/logout")
    public ResponseEntity<String> logout() {
        Subject subject = SecurityUtils.getSubject();

        User user = (User) subject.getPrincipal();
        userService.logout(user.getUsername());
        subject.logout();

        return ResponseEntity.ok().build();
    }
}

UserServiceImpl

/**
 * @author passerbyYSQ
 * @create 2020-08-21 11:02
 */
@Service("userService") // 不要忘了
@Transactional // 开启事务。有需要再开启
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    public String generateJwt(String username) {
        // 8个字符的随机字符串,作为生成jwt的随机盐
        // 保证每次登录成功返回的Token都不一样
        String jwtSecret = RandomUtil.generateStr(8);
        // 将此次登录成功的jwt secret存到数据库,下次携带jwt时解密需要使用
        userDAO.updateJwtSecretByUsername(username, jwtSecret);
        return JwtUtil.generateJwt(username, jwtSecret);
    }

    @Override
    public User findByUsername(String username) {
        return userDAO.findByUsername(username);
    }

    @Override
    public void logout(String username) {
        // 将jwt secret置为空
        userDAO.updateJwtSecretByUsername(username, "");
    }

}

LoginRealm:处理密码登录的Realm,复写doGetAuthenticationInfo()方法

/**
 * @author passerbyYSQ
 * @create 2020-08-20 23:31
 */
public class LoginRealm extends AuthorizingRealm {

    /*
    如果@Autowired,需要在当前类前加上@Componet注解,将当前类的实例注入到IOC容器
    但是如果有多个类似的类,都要注册到容器中,不太好。我可以新建一个管理类,注册到容器中,
    为我们统一获取@Autowired的实例
     */
//    @Autowired
//    private UserService userService;


    /**
     * 或者在ShiroConfig中设置
     */
    public LoginRealm() {
        // 匹配器。需要与密码加密规则一致
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 设置匹配器的加密算法
        hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
        // 设置匹配器的哈希散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        // 将对应的匹配器设置到Realm中
        this.setCredentialsMatcher(hashedCredentialsMatcher);
    }

    /**
     * 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
     * 复写该方法表示该方法支持处理哪一种Token
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
        String principal = (String) token.getPrincipal();
        // 从IOC容器中获取UserService组件
        UserService userService = (UserService) ApplicationContextUtil.getBean("userService");

        User user = userService.findByUsername(principal);

        if (!ObjectUtils.isEmpty(user)) {
            // 返回正确的信息(数据库存储的),作为比较的基准
            return  new SimpleAuthenticationInfo(
                    user, user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), this.getName()
            );
        }

        return null;
    }
}

密码登录的主要流程基本就这么多。但是还需要写配置类将LoginRealm注册到Shiro中,这个后面统一把 Shiro 的配置类贴出来。上面代码还涉及到两个工具类:

ApplicationContextUtil:用于更加灵活地获取IOC容器中组件

JwtUtil:jwt的工具类,提供几个主要的静态方法,包括:生成jwt,校验jwt,获取jwt里面的数据,获取jwt的签发时间。

ApplicationContextUtil

/**
 * @author passerbyYSQ
 * @create 2020-08-21 11:50
 */
@Component // 加入容器
public class ApplicationContextUtil implements ApplicationContextAware {

    // IOC容器
    private static ApplicationContext context;


    /**
     * 将IOC容器回调给我们,我们将它缓存起来
     *
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    /**
     * 从IOC容器中获取组件(bean)
     *
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName) {
        return context.getBean(beanName);
    }
}

JwtUtil

/**
 * JWT的工具类,包括签发、验证、获取信息
 *
 * @author passerbyYSQ
 * @create 2020-08-22 11:13
 */
public class JwtUtil {

    // 有效时间:7天
    private static final long EFFECTIVE_DURATION = 1000 * 60 * 60 * 24 * 7;
    // 发行者
    private static final String ISSUER = "net.ysq";

    /**
     * 生成Jwt字符串
     *
     * @param claims    由于类库只支持基本类型的包装类、String、Date,我们最好使用String
     * @param secret    加密的密钥
     * @return
     */
    public static String generateJwt(Map<String, String> claims, String secret) {
        // 发行时间
        Date issueAt = new Date();
        // 过期时间
        Date expireAt = new Date(issueAt.getTime() + EFFECTIVE_DURATION);
        // 加密算法
        Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));

        JWTCreator.Builder builder = JWT.create()
                .withIssuer(ISSUER)
                .withIssuedAt(issueAt)
                .withExpiresAt(expireAt);

        // 设置Payload信息
        Set<String> keySet = claims.keySet();
        for (String key : keySet) {
            builder.withClaim(key, claims.get(key));
        }

        return builder.sign(algorithm);
    }

    public static String generateJwt(String username, String secret) {
        Map<String, String> claims = new HashMap<>();
        claims.put("username", username);
        return generateJwt(claims, secret);
    }

    /**
     * 校验jwt是否合法
     *
     * @param jwt
     * @param claims
     * @return
     */
    public static boolean verifyJwt(String jwt, Map<String, String> claims, String secret) {
        // 解密算法
        Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
        try {
            Verification verification = JWT.require(algorithm).withIssuer(ISSUER);

            Set<String> keySet = claims.keySet();
            for (String key : keySet) {
                verification.withClaim(key, claims.get(key));
            }

            JWTVerifier verifier = verification.build();
            verifier.verify(jwt);

            return true;
        } catch (IllegalArgumentException | JWTVerificationException e) {
            e.printStackTrace();
        }
        return false;
    }

    public static boolean verifyJwt(String jwt, String username, String secret) {
        Map<String, String> claims = new HashMap<>();
        claims.put("username", username);
        return verifyJwt(jwt, claims, secret);
    }

    /**
     * 根据key获取claim值
     *
     * @param jwt
     * @param key
     * @return
     */
    public static String getClaimByKey(String jwt, String key) {
        try {
            DecodedJWT decodedJwt = JWT.decode(jwt);
            return decodedJwt.getClaim(key).asString(); // 注意不要用toString
        } catch (JWTDecodeException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 返回过期的时间
     * 
     * @param jwt
     * @return
     */
    public static Date getExpireAt(String jwt) {
        try {
            DecodedJWT decodedJwt = JWT.decode(jwt);
            return decodedJwt.getExpiresAt();
        } catch (JWTDecodeException e) {
            e.printStackTrace();
        }
        return null;
    }
}

四、携带jwt访问

JwtToken:类比UsernamePasswordToken

/**
 * 或者直接实现AuthenticationToken也可以,不需要host
 *
 * @author passerbyYSQ
 * @create 2020-08-22 10:42
 */
public class JwtToken implements HostAuthenticationToken {

    // JWT字符串
    private String token;

    private String host;

    public JwtToken(String token) {
        this(token, null);
    }

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

    @Override
    public String getHost() {
        return token;
    }

    /**
     * 返回身份信息(相当于username),这个方法的返回比较重要,前面的代码也说到了
     * Jwt里面包含一个访问主体的身份(比如说username)
     * @return
     */
    @Override
    public Object getPrincipal() {
        return token;
    }

    /**
     * 返回凭证信息(相当于password)
     * Jwt本身就是一个令牌凭证,在服务端通过解密校验
     * @return
     */
    @Override
    public Object getCredentials() {
        return token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public void setHost(String host) {
        this.host = host;
    }
}

JwtAuthenticatingFilter:全局请求的过滤器。驳回没有携带token的请求,不能访问受限资源。对于携带token的请求,校验token是否有效,有效最重放行到controller层

/**
 * @author passerbyYSQ
 * @create 2020-08-22 12:06
 */
public class JwtAuthenticatingFilter extends BasicHttpAuthenticationFilter {

    // 是否刷新token
    private boolean shouldRefreshToken;

    public JwtAuthenticatingFilter() {
        this.shouldRefreshToken = false;
    }

    /**
     * 请求是否允许放行
     * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch(IllegalStateException e){ //not found any token
            System.out.println("Not found any token");
        }catch (Exception e) {
            System.out.println("Error occurs when login");
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /**
     * 父类executeLogin()首先会createToken(),然后调用shiro的Subject.login()方法。
     *
     * executeLogin()的逻辑是不是跟UserController里面的密码登录逻辑很像?
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 从请求头中的Authorization字段尝试获取jwt token
        String token = httpRequest.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            // 从请求头中的token字段(自定义字段)尝试获取jwt token
            token = httpRequest.getHeader("token");
        }
        if (StringUtils.isEmpty(token)) {
            // 从url参数中尝试获取jwt token
            token = httpRequest.getParameter("token");
        }

        if (!StringUtils.isEmpty(token)) {
            return new JwtToken(token);
        }

        return null;
    }

    /**
     * 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
        PrintWriter writer = response.getWriter();
        writer.print("无效token");
        fillCorsHeader(request, httpResponse);
        return false;
    }

    /**
     * 登录成功后判断是否需要刷新token
     * 登录成功说明:jwt有效,尚未过期。当离过期时间不足一天时,往响应头中放入新的token返回给前端
     *
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) {

        String oldToken = (String) token.getPrincipal();

        Date expireAt = JwtUtil.getExpireAt(oldToken);
        int countDownDays = (int) DateTimeUtil.differDaysBetween(
                LocalDateTime.now(), DateTimeUtil.toLocalDateTime(expireAt));

        if (shouldRefreshToken && !ObjectUtils.isEmpty(expireAt)
             && countDownDays < 1) {  // 如果离过期时间不足一天

            UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
            User user = (User) subject.getPrincipal();
            String newToken = userService.generateJwt(user.getUsername());
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.addHeader("token", newToken);
        }

        return true;
    }

    /**
     * 添加跨域支持
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) {
        fillCorsHeader(request, response);
    }

    /**
     * 设置跨域
     */
    public void fillCorsHeader(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setStatus(HttpStatus.OK.value());
        }
    }

    public boolean isShouldRefreshToken() {
        return shouldRefreshToken;
    }

    public void setShouldRefreshToken(boolean shouldRefreshToken) {
        this.shouldRefreshToken = shouldRefreshToken;
    }
}

JwtRealm

/**
 * @author passerbyYSQ
 * @create 2020-08-23 18:24
 */
public class JwtRealm extends AuthorizingRealm {

    public JwtRealm() {
        // 用我们自定的Matcher
        this.setCredentialsMatcher(new JwtCredentialsMatcher());
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//        JwtToken jwtToken = (JwtToken) token;
//        String tokenStr = jwtToken.getToken();

        // 取决于JwtToken的getPrincipal()
        String tokenStr = (String) token.getPrincipal();

        // 从jwt字符串中解析出username信息
        String username = JwtUtil.getClaimByKey(tokenStr, "username");
        if (!Strings.isEmpty(username)) {
            UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
            // 根据token中的username去数据库核对信息,返回用户信息,并封装称SimpleAuthenticationInfo给Matcher去校验
            User user = userService.findByUsername(username);
            // principle是身份信息,简单的可以放username,也可以将User对象作为身份信息
            // 身份信息可以在登录成功之后通过subject.getPrinciple()取出
            return new SimpleAuthenticationInfo(user, user.getJwtSecret(), this.getName());
        }

        return null;
    }
}

JwtCredentialsMatcher:Jwt的比较器

/**
 * @author passerbyYSQ
 * @create 2020-08-23 18:42
 */
public class JwtCredentialsMatcher implements CredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        //  AuthenticationInfo info 是我们在JwtRealm中doGetAuthenticationInfo()返回的那个
        User user = (User) info.getPrincipals().getPrimaryPrincipal();
        String secret = (String) info.getCredentials();

//        String tokenStr = ((JwtToken) token).getToken();
        String tokenStr = (String) token.getPrincipal();

        // 校验jwt有效
        return JwtUtil.verifyJwt(tokenStr, user.getUsername(), secret);
    }
}

五、Shiro的自定义配置类

总结来说,这个配置类主要干两种事情:

1、将我们上述定义的Realm,过滤器等,注册给Shiro

2、设置请求的拦截规则

代码解析细节,参看注释。个人强迫症,注释写的还算是比较可以的。

/**
 * 整合Shiro框架的配置类
 *
 * @author passerbyYSQ
 * @create 2020-08-20 23:10
 */
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(
            DefaultWebSecurityManager securityManager, ShiroFilterChainDefinition chainDefinition) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 必须的设置。我们自定义的Realm此时已经被设置到securityManager中了
        factoryBean.setSecurityManager(securityManager);

        // 注册我们写的过滤器
        Map<String, Filter> filters = factoryBean.getFilters();
        filters.put("jwtAuth", new JwtAuthenticatingFilter());

        factoryBean.setFilters(filters);

        // 设置请求的过滤规则。其中过滤规则中用到了我们注册的过滤器:jwtAuth
        factoryBean.setFilterChainDefinitionMap(chainDefinition.getFilterChainMap());

        return factoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(Authenticator authenticator) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 所有的Realm都用这个全局缓存。不生效,需要在realm中设置缓存。原因暂时搞不懂。
//        securityManager.setCacheManager(new EhCacheManager());
        securityManager.setAuthenticator(authenticator);
        return securityManager;
    }

    /**
     * 设置请求的过滤规则
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/user/register", "noSessionCreation,anon");
        chainDefinition.addPathDefinition("/user/login", "noSessionCreation,anon");  //login不做认证,noSessionCreation的作用是用户在操作session时会抛异常

        // 注意第2个参数的"jwtAuth"需要与上面的 filters.put("jwtAuth", new JwtAuthenticatingFilter()); 一致
        chainDefinition.addPathDefinition("/user/logout", "noSessionCreation,jwtAuth[permissive]"); //做用户认证,permissive参数的作用是当token无效时也允许请求访问,不会返回鉴权未通过的错误
        chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtAuth"); // 默认进行用户鉴权
        return chainDefinition;
    }

    /**
     * 初始化Authenticator,将我们需要的Realm设置进去
     * Shiro会将Authenticator设置到SecurityManager里面
     */
    @Bean
    public Authenticator authenticator(@Qualifier("loginRealm") Realm loginRealm, @Qualifier("jwtRealm") Realm jwtRealm) {

        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(loginRealm, jwtRealm));
        //设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }


    /**
     * 返回我们自定义的Realm
     *
     * @return
     */
    @Bean("loginRealm") // 自动配置类中有同名组件,如果只写@Bean,会出现歧义
    public Realm loginRealm(EhCacheManager ehCacheManager) {
        LoginRealm loginRealm = new LoginRealm();

        // AuthenticatingRealm里面的isAuthenticationCachingEnabled()
        loginRealm.setCacheManager(ehCacheManager);
        loginRealm.setCachingEnabled(true); // 这句话不能少!!!
        loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
        loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存

        return loginRealm;
    }

    @Bean("jwtRealm")
    public Realm jwtRealm(EhCacheManager ehCacheManager) {
        JwtRealm jwtRealm = new JwtRealm();

        jwtRealm.setCacheManager(ehCacheManager);
        jwtRealm.setCachingEnabled(true);  // 这句话不能少!!!
        jwtRealm.setAuthenticationCachingEnabled(true); // 认证缓存
        jwtRealm.setAuthorizationCachingEnabled(true); // 授权缓存

        return jwtRealm;
    }

    /**
     * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
     * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,
     * 如果要完全禁用,要配合上过滤规则的noSessionCreation的Filter来实现
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    /**
     * shiro的全局缓存管理器
     * @return
     */
    @Bean
    public EhCacheManager ehCacheManager() {
        return new EhCacheManager();
    }

}

博客注重思路逻辑分析。有需要的同学可以去Github自取源码。https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt

求赞和收藏。。。么么哒

猜你喜欢

转载自blog.csdn.net/qq_43290318/article/details/108225519
今日推荐