Shiro 整合JWT篇09

功能:登录成功时返回token,之后的请求携带token进行认证授权;之前是认证一次,由session存储用户信息,现在是使用jwt(无状态)每次请求都需要重新认证

1 github:https://github.com/yezhimincxvxb/shiro/tree/master/shiro09

2 shiro09 子工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.yzm</groupId>
        <artifactId>shiro</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>

    <artifactId>shiro09</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>shiro09</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>com.yzm</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

项目结构
Shiro 整合JWT篇09
application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test3?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: root

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.yzm.shiro09.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3 自定义JwtToken

之前是通过用户名和密码进行认证,使用的是UsernamePasswordToken
现在第一次登录认证还是用户名和密码,之后的请求进行认证就使用我们自定义的JwtToken
JwtToken.java

package com.yzm.shiro09.entity;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken implements AuthenticationToken {
    private static final long serialVersionUID = -4763390136373610135L;
    private String token;

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

    public String getToken() {
        return token;
    }

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

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

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

Shiro通过supports()来决定使用哪块Realm进行认证

/**
     * 限定这个Realm只支持我们自定义的JWT Token
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

4 自定义拦截器JwtFilter,指定哪些url需要携带token并生成JwtToken

isAccessAllowed() 该拦截器的入口

createToken() 获取请求头的token并判断是否过期

JwtFilter.java

package com.yzm.shiro09.config;

import com.alibaba.fastjson.JSONObject;
import com.yzm.common.entity.HttpResult;
import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class JwtFilter extends AuthenticatingFilter {

    /**
     * 跨域支持
     * 前置处理
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 跨域支持
     * 后置处理
     */
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }

    /**
     * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch (IllegalStateException e) {
            log.error("Not found any token");
        } catch (Exception e) {
            log.error("Error occurs when login", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        } else {
            try {
                Subject subject = this.getSubject(request, response);
                subject.login(token);
                return this.onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException var5) {
                return this.onLoginFailure(token, var5, request, response);
            }
        }
    }

    /**
     * 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String jwtToken = JwtUtils.getTokenFromRequest(WebUtils.toHttp(request));
        if (StringUtils.isNotBlank(jwtToken)) return new JwtToken(jwtToken);
        return null;
    }

    /**
     * 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        HttpResult result = new HttpResult(500, "请求失败", null);
        response.getWriter().print(JSONObject.toJSONString(result, true));
        response.getWriter().flush();
        response.getWriter().close();
        return false;
    }

    /**
     * 如果Shiro Login认证成功,会进入该方法,等同于用户名密码登录成功,我们这里还判断了是否要刷新Token
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        log.info("token认证成功");
        if (token instanceof JwtToken) {
            JwtToken jwtToken = (JwtToken) token;
            String tokenStr = jwtToken.getToken();
            if (JwtUtils.isExpired(tokenStr)) {
                log.info("刷新token");
                String username = (String) subject.getPrincipal();
                Map<String, Object> map = new HashMap<>();
                map.put(JwtUtils.USERNAME, username);
                tokenStr = JwtUtils.generateToken(map);
            }
            HttpServletResponse httpResponse = WebUtils.toHttp(response);
            httpResponse.setHeader("token", tokenStr);
        }
        return true;
    }

    /**
     * 如果调用shiro的login认证失败,会回调这个方法
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.info("token认证失败");
        return false;
    }

}

JwtFilter拦截器的处理逻辑跟登录接口是差不多的,生成对应的token并调用subject.login(token)

    @PostMapping("doLogin")
    @ResponseBody
    public Object doLogin(@RequestParam String username, @RequestParam String password, boolean rememberMe) {
        String token;
        try {
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
            if (rememberMe) usernamePasswordToken.setRememberMe(true);
            Subject subject = SecurityUtils.getSubject();
            subject.login(usernamePasswordToken);

            // 生成token
            Map<String, Object> map = new HashMap<>();
            map.put(JwtUtils.USERNAME, username);
            token = JwtUtils.generateToken(map);
        } catch (IncorrectCredentialsException ice) {
            return HttpResult.error("password error!");
        } catch (UnknownAccountException uae) {
            return HttpResult.error("username error!");
        }

        return HttpResult.ok(token);
    }

在ShrioConfig#shiroFilter,注入到Shiro并使用

    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置无权限时跳转的 url
        shiroFilterFactoryBean.setUnauthorizedUrl("/401");

        // 注入自定义拦截器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("autht", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // url拦截规则
        Map<String, String> definitionMap = new LinkedHashMap<>();
        definitionMap.put("/home", "anon");
        definitionMap.put("/register", "anon");
        definitionMap.put("/login", "anon");
        definitionMap.put("/doLogin", "anon");
        definitionMap.put("/401", "anon");
        definitionMap.put("/logout", "anon");

        // 禁用session创建,还需要noSessionCreation
        definitionMap.put("/**", "noSessionCreation,autht");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
        return shiroFilterFactoryBean;
    }

5 JwtUtils token工具类

package com.yzm.shiro09.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtils implements Serializable {

    private static final long serialVersionUID = 8527289053988618229L;
    /**
     * token头
     * token前缀
     */
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Basic ";
    /**
     * 用户名称
     */
    public static final String USERNAME = Claims.SUBJECT;
    public static final String PASSWORD = "password";
    /**
     * 权限列表
     */
    public static final String AUTHORITIES = "authorities";
    /**
     * 密钥
     */
    private static final String SECRET = "abcdefg";
    private static final String JWT_SECRET = "7786df7fc3a34e26a61c034d5ec8245d";
    /**
     * 过期时间5分钟
     * 刷新时间2分钟
     */
    public static final long TOKEN_EXPIRED_TIME = 5 * 60 * 1000L;
    public static final long TOKEN_REFRESH_TIME = 2 * 60 * 1000L;

    public static String generateToken(Map<String, Object> claims) {
        return generateToken(claims, 0L);
    }

    /**
     * 生成令牌
     */
    public static String generateToken(Map<String, Object> claims, long expireTime) {
        if (expireTime <= 0L) expireTime = TOKEN_EXPIRED_TIME;

        Map<String, Object> headMap = new HashMap<>();
        headMap.put("typ", "JWT");
        headMap.put("alg", "HS256");

        return Jwts.builder()
                .setHeader(headMap)
                //JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
                .setId(UUID.randomUUID().toString())
                //.setIssuer("该JWT的签发者,是否使用是可选的")
                //.setSubject("该JWT所面向的用户,是否使用是可选的")
                //.setAudience("接收该JWT的一方,是否使用是可选的")
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //签发时间(token生成时间)
                .setIssuedAt(new Date())
                //生效时间(在指定时间之前令牌是无效的)
                .setNotBefore(new Date())
                //过期时间(在指定时间之后令牌是无效的)
                .setExpiration(new Date(System.currentTimeMillis() + expireTime))
                //设置签名使用的签名算法和签名使用的秘钥
//                .signWith(SignatureAlgorithm.HS256, JWT_SECRET)
                .signWith(SignatureAlgorithm.HS256, generalKey())
                .compact();
    }

    /**
     * 验证令牌
     */
    public static Claims verifyToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    //签名秘钥
//                    .setSigningKey(JWT_SECRET)
                    .setSigningKey(generalKey())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            // token过期是直接抛出异常的,但仍然可以获取到claims对象
            claims = e.getClaims();
        }
        return claims;
    }

    /**
     * 由密钥生成加密key
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.decodeBase64(SECRET);
//        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return new SecretKeySpec(encodedKey, SignatureAlgorithm.HS256.getJcaName());
    }

    /**
     * 是否过期
     * true:过期
     * false:未过期
     */
    public static boolean isExpired(String token) {
        Claims claims = verifyToken(token);
        //和当前时间进行对比来判断是否过期
        return claims.getExpiration().before(new Date());
    }

    /**
     * 从令牌中获取用户名
     */
    public static String getUsernameFromToken(String token) {
        Claims claims = verifyToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     */
    public static String getTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);
        if (token == null) token = request.getHeader("token");
        if (StringUtils.isBlank(token)) return null;

        if (token.startsWith(TOKEN_PREFIX)) {
            token = token.substring(TOKEN_PREFIX.length());
        }
        return token;
    }
}

6 认证和授权

MyShiroRealm.java

package com.yzm.shiro09.config;

import com.yzm.shiro09.entity.Permissions;
import com.yzm.shiro09.entity.Role;
import com.yzm.shiro09.entity.User;
import com.yzm.shiro09.service.PermissionsService;
import com.yzm.shiro09.service.RoleService;
import com.yzm.shiro09.service.UserService;
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.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 自定义realm
 * 实现身份认证和权限授权
 */
public class MyShiroRealm extends AuthorizingRealm {

    private final UserService userService;
    private final RoleService roleService;
    private final PermissionsService permissionsService;

    public MyShiroRealm(UserService userService, RoleService roleService, PermissionsService permissionsService) {
        this.userService = userService;
        this.roleService = roleService;
        this.permissionsService = permissionsService;
    }

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

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) principalCollection.getPrimaryPrincipal();
        // 查询用户,获取角色ids
        User user = userService.lambdaQuery().eq(User::getUsername, username).one();
        List<Integer> roleIds = Arrays.stream(user.getRIds().split(","))
                .map(Integer::parseInt)
                .collect(Collectors.toList());

        // 查询角色,获取角色名、权限ids
        List<Role> roles = roleService.listByIds(roleIds);
        Set<String> roleNames = new HashSet<>(roles.size());
        Set<Integer> permIds = new HashSet<>();
        roles.forEach(role -> {
            roleNames.add(role.getRName());
            Set<Integer> collect = Arrays.stream(
                    role.getPIds().split(",")).map(Integer::parseInt).collect(Collectors.toSet());
            permIds.addAll(collect);
        });

        // 获取权限名称
        List<Permissions> permissions = permissionsService.listByIds(permIds);
        List<String> permNames = permissions.stream().map(Permissions::getPName).collect(Collectors.toList());

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(roleNames);
        authorizationInfo.addStringPermissions(permNames);
        return authorizationInfo;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户名跟密码
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        String username = usernamePasswordToken.getUsername();
        // 也可以这样获取
        //String username = (String) authenticationToken.getPrincipal();
        //String password = new String((char[]) authenticationToken.getCredentials());

        // 查询用户是否存在
        User user = userService.lambdaQuery().eq(User::getUsername, username).one();
        if (user == null) {
            throw new UnknownAccountException();
        }

        return new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                // 用户名 + 盐
                new MySimpleByteSource(user.getUsername() + user.getSalt()),
                getName()
        );
    }
}

JwtRealm.java

package com.yzm.shiro09.config;

import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.entity.User;
import com.yzm.shiro09.service.UserService;
import com.yzm.shiro09.utils.JwtUtils;
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;

public class JwtRealm extends AuthorizingRealm {

    private final UserService userService;

    public JwtRealm(UserService userService) {
        this.userService = userService;
    }

    /**
     * 限定这个Realm只支持我们自定义的JWT Token
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

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

        String username = JwtUtils.getUsernameFromToken(token);
        if (username == null) {
            throw new AuthenticationException("token过期,请重新登录");
        }

        User user = userService.lambdaQuery().eq(User::getUsername, username).one();
        if (user == null) {
            throw new AuthenticationException("账号异常");
        }

        return new SimpleAuthenticationInfo(
                username,
                token,
                getName());
    }

    /**
     * 由于在MyShiroRealm#doGetAuthorizationInfo中以及进行授权了,所以这里可以直接返回空的角色权限
     *
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return new SimpleAuthorizationInfo();
    }
}

JwtRealm的凭证信息是token,所以我们需要自定义凭证匹配器JwtCredentialsMatcher,主要就是校验下token是否过期
JwtCredentialsMatcher.java

package com.yzm.shiro09.config;

import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;

/**
 * 校验token是否过期
 */
    public class JwtCredentialsMatcher implements CredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
        if (authenticationToken instanceof JwtToken) {
            String token = (String) authenticationInfo.getCredentials();
            return !JwtUtils.isExpired(token);
        }
        return false;
    }
}

7 ShiroConfig 配置类

package com.yzm.shiro09.config;

import com.yzm.shiro09.service.PermissionsService;
import com.yzm.shiro09.service.RoleService;
import com.yzm.shiro09.service.UserService;
import com.yzm.shiro09.utils.EncryptUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.servlet.Filter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
public class ShiroConfig {

    private final UserService userService;
    private final RoleService roleService;
    private final PermissionsService permissionsService;

    public ShiroConfig(UserService userService, RoleService roleService, PermissionsService permissionsService) {
        this.userService = userService;
        this.roleService = roleService;
        this.permissionsService = permissionsService;
    }

    /**
     * 凭证匹配器
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName(EncryptUtils.ALGORITHM_NAME);
        hashedCredentialsMatcher.setHashIterations(EncryptUtils.HASH_ITERATIONS);
        //hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 用户realm
     */
    @Bean
    public MyShiroRealm shiroRealm() {
        MyShiroRealm shiroRealm = new MyShiroRealm(userService, roleService, permissionsService);
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

        // 开启缓存
        shiroRealm.setCachingEnabled(true);
        //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
        shiroRealm.setAuthenticationCachingEnabled(true);
        //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
        shiroRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存,即缓存AuthorizationInfo信息,默认false
        shiroRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置
        shiroRealm.setAuthorizationCacheName("authorizationCache");
        return shiroRealm;
    }

    /**
     * jwtRealm
     */
    @Bean
    public JwtRealm jwtRealm() {
        JwtRealm jwtRealm = new JwtRealm(userService);
        jwtRealm.setCredentialsMatcher(new JwtCredentialsMatcher());
        return jwtRealm;
    }

    /**
     * 记住我功能
     */
    @Bean
    public Cookie simpleCookie() {
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        //设为true后,只能通过http访问,javascript无法访问
        //防止xss读取cookie
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        //存活时间,单位秒;-1表示关闭浏览器该cookie失效
        cookie.setMaxAge(-1);
        return cookie;
    }

    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        rememberMeManager.setCookie(simpleCookie());
        //cookie加密的密钥
        //rememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return rememberMeManager;
    }

    /**
     * 禁用session, 不保存用户登录状态。保证每次请求都重新认证
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    /**
     * redis缓存
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("127.0.0.1:6379");
        redisManager.setPassword("1234");
        redisManager.setDatabase(0);
        return redisManager;
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        // redis中针对不同用户缓存
        redisCacheManager.setPrincipalIdFieldName("username");
        // 用户权限信息缓存时间
        redisCacheManager.setExpire(300);
        return redisCacheManager;
    }

    /**
     * 安全管理
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置单个realm
        //securityManager.setRealm(shiroRealm());

        // 验证器
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        securityManager.setAuthenticator(authenticator);

        // 自定义realm 一定要放在securityManager.authorizer赋值之后(因为调用setRealms会将realms设置给authorizer
        securityManager.setRealms(Arrays.asList(shiroRealm(), jwtRealm()));

        // 记住我
        securityManager.setRememberMeManager(rememberMeManager());

        // 缓存
        securityManager.setCacheManager(redisCacheManager());

        // 关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 开启注解方式控制访问url
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置无权限时跳转的 url
        shiroFilterFactoryBean.setUnauthorizedUrl("/401");

        // 注入自定义拦截器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("autht", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // url拦截规则
        Map<String, String> definitionMap = new LinkedHashMap<>();
        definitionMap.put("/home", "anon");
        definitionMap.put("/register", "anon");
        definitionMap.put("/login", "anon");
        definitionMap.put("/doLogin", "anon");
        definitionMap.put("/401", "anon");
        definitionMap.put("/logout", "anon");

        // 禁用session创建,还需要noSessionCreation
        definitionMap.put("/**", "noSessionCreation,autht");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 解决: 登录页以及无权限页面不跳转问题
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        // 登录后没有权限跳转到/401
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/401");
        // 未登录访问接口跳转到/login
        properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/login");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }
}

测试

登录yzm
Shiro 整合JWT篇09
访问/user/select
Shiro 整合JWT篇09
在这里我们设置了不创建Session

@Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ...

        // 注入自定义拦截器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("autht", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // url拦截规则
        ...

        // 禁用session创建,还需要noSessionCreation
        definitionMap.put("/**", "noSessionCreation,autht");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
        return shiroFilterFactoryBean;
    }

并且在UserController.java中获取会话

    @GetMapping
    public Object user() {
        // 当使用noSessionCreation时,这里就会报错
        System.out.println("会话ID:" + SecurityUtils.getSubject().getSession().getId());
        return SecurityUtils.getSubject().getPrincipal();
    }

访问/user,会报错提示Session creation has been disabled for the current subject(已为当前主题禁用会话创建)
Shiro 整合JWT篇09

上一篇:【爆肝推荐】手摸手带你做Springboot + mybatis plus + shiro + redis 后台管理项目(第三章)整合shiro 设置用户密码加密


下一篇:《潮流时装设计——世界*时装CAD制板技巧》——1.7 绘图符号