SpringBoot + Shiro + Jwt para lograr la autenticación de inicio de sesión, análisis de código

Prefacio

Tomó unos días comprender el marco de trabajo de Shiro (no demasiado en profundidad), e hice una demostración basada en información en línea: SpringBoot 2.2.9.RELEASE + Shiro + Jwt para lograr la autenticación de inicio de sesión.

1. Esta demostración se centra en la autorización de inicio de sesión (más general) y básicamente no implica el control de permisos, porque el control de permisos está diseñado para empresas específicas. Por lo tanto, esta demostración se puede modificar ligeramente de acuerdo con su situación real y se puede utilizar como un módulo de inicio de sesión para proyectos de separación de front-end y back-end.

2. Este blog es adecuado para personas que tienen un conocimiento de Shiro y Jwt. Al menos tienen un conocimiento de las funciones y métodos de algunas clases clave. No lo expandiré a continuación, solo analizaré las ideas y códigos del Manifestación.

Análisis de pensamiento

1. Proceso de autenticación de inicio de sesión original de Shiro

subject.login (new UsernamePasswordToken (nombre de usuario, contraseña)); Este es el código clave para realizar la autenticación de inicio de sesión, y luego Shiro encontrará la información de usuario correcta (incluida la información de contraseña) en la base de datos según el nombre de usuario en doGetAuthenticationInfo () en el Reino correspondiente , la contraseña se puede cifrar y almacenar). Luego, encapsule la información de usuario correcta en un objeto AuthenticationInfo. En el método doCredentialsMatch () del comparador (CredentialsMatcher), la coincidencia de contraseñas (comparación) se realiza de acuerdo con ciertas reglas. Si la coincidencia es exitosa, el inicio de sesión es exitoso.

Una vez que el inicio de sesión es exitoso, la información de inicio de sesión del sujeto se almacenará en la sesión. Luego, la próxima vez que acceda a recursos o interfaces restringidos, no es necesario que vuelva a iniciar sesión.

2. El proceso de autenticación de inicio de sesión después de unirse a Jwt

¿Existe un conflicto entre Jwt y la autenticación de inicio de sesión original de Shiro? De hecho, Jwt es esencialmente una ficha especial. En otras palabras, Shiro + Jwt significa usar token (Jwt) para reemplazar la sesión original de Shiro, y el servidor que usa token es más adecuado para proyectos donde el front y el back end están separados. No importa si la interfaz es un proyecto vue o una aplicación de Android, etc.

¿Cuál es el proceso después del reemplazo? En primer lugar, para el primer inicio de sesión, necesitamos que el usuario ingrese el nombre de usuario y la contraseña (de acuerdo con la situación real, aquí hay un ejemplo simple) y luego inicie sesión de acuerdo con el proceso de autenticación de inicio de sesión original de Shiro. Si el inicio de sesión es exitoso, el servidor devuelve una cadena Jwt (equivalente a emitir un token) al front-end. La próxima vez que el usuario acceda a los recursos restringidos del servidor, podrá acceder a él siempre que lleve el Jwt correcto y legal.

¿Cuál es la idea principal de realización? Necesitamos implementar un filtro para interceptar todas las solicitudes. Para el procesamiento especial de solicitudes con Jwt en el encabezado de la solicitud, no usamos el proceso de inicio de sesión predeterminado de Shiro para procesar, sino nuestro proceso de procesamiento personalizado, incluida la implementación del Token correspondiente, Realm , CredentialsMatcher. Una vez pasada la autenticación, la solicitud se envía a la capa del controlador.

 Después de un análisis, esta demostración se divide en tres partes: registro , inicio de sesión con contraseña y acceso con token (jwt) . Entre ellos, la parte de "acceso a token de acarreo (jwt)" es la lógica central de Shiro para integrar Jwt.

Análisis de código central

Uno, importación de dependencias y tabla de base de datos

No publicaré otras dependencias, como las relacionadas con la base de datos y otras dependencias. Para obtener más detalles, consulte el código fuente de Github de la demostración: 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>

2. Registro

Al registrarnos, necesitamos encriptar la contraseña de texto plano ingresada por el usuario a través de Md5Hash, y pasar el salt aleatorio encriptado (diferente para cada usuario, debe almacenarse en la tabla de usuario) y el número de hashes (el mismo para cada usuario).

Luego, cuando usamos una contraseña para iniciar sesión, necesitamos encriptar la contraseña ingresada por el usuario con las mismas reglas de encriptación, y luego compararla con la contraseña encriptada almacenada en la base de datos durante el registro. Si es el mismo, el inicio de sesión es exitoso.

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);
    }
}

Tres, inicio de sesión con contraseña

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: Reino que maneja el inicio de sesión con contraseña, sobrescribe el método 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;
    }
}

El proceso principal de inicio de sesión con contraseña es básicamente mucho. Sin embargo, también necesita escribir una clase de configuración para registrar LoginRealm en Shiro, y esto publicará la clase de configuración de Shiro de manera uniforme. El código anterior también involucra dos clases de herramientas:

ApplicationContextUtil: para un acceso más flexible a los componentes en el contenedor IOC

JwtUtil: la clase de herramienta de jwt, que proporciona varios métodos estáticos principales, que incluyen: generar jwt, verificar jwt, obtener datos en jwt y obtener el tiempo de emisión de 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;
    }
}

Cuatro, llevar acceso jwt

JwtToken: analogía con 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: filtro de solicitud global. Rechace las solicitudes que no llevan tokens y no pueden acceder a recursos restringidos. Para solicitudes con tokens, verifique que los tokens sean válidos y reprodúzcalos en la capa del controlador de manera más efectiva.

/**
 * @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: comparador de 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);
    }
}

Cinco, la clase de configuración personalizada de Shiro

En resumen, esta clase de configuración hace principalmente dos cosas:

1. Registre el Reino, filtro, etc. que definimos anteriormente para Shiro

2. Establecer la regla de interceptación de solicitudes

Para obtener detalles sobre el análisis de código, consulte los comentarios. Trastorno obsesivo compulsivo personal, los comentarios son bastante aceptables.

/**
 * 整合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();
    }

}

El blog se centra en el análisis lógico de ideas. Los estudiantes que lo necesiten pueden ir a Github para recoger el código fuente. https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt

Por favor, me gusta y favoritos. . . muah

Supongo que te gusta

Origin blog.csdn.net/qq_43290318/article/details/108225519
Recomendado
Clasificación