SpringBoot + Shiro + Jwt pour réaliser l'authentification de connexion, l'analyse de code

Préface

Il a fallu quelques jours pour comprendre le framework Shiro (pas trop en profondeur), et faire une démo basée sur des informations en ligne: SpringBoot 2.2.9.RELEASE + Shiro + Jwt pour obtenir l'authentification de connexion.

1. Cette démo se concentre sur l'autorisation de connexion (plus générale) et n'implique fondamentalement pas de contrôle des autorisations, car le contrôle des autorisations est conçu pour des entreprises spécifiques. Par conséquent, cette démo peut être légèrement modifiée en fonction de sa situation réelle, et elle peut être utilisée comme module de connexion pour les projets de séparation front-end et back-end.

2. Ce blog convient aux personnes qui ont une compréhension de Shiro et Jwt. Au moins, elles ont une compréhension des fonctions et des méthodes de quelques classes clés. Je ne développerai pas ci-dessous, mais analyserai seulement les idées et les codes du Démo.

Analyse de la pensée

1. Processus d'authentification de connexion d'origine de Shiro

subject.login (new UsernamePasswordToken (username, password)); Ceci est le code clé pour effectuer l'authentification de connexion, puis Shiro trouvera les informations utilisateur correctes (y compris les informations de mot de passe) dans la base de données en fonction du nom d'utilisateur dans doGetAuthenticationInfo () dans le royaume correspondant , le mot de passe peut être crypté et stocké). Encapsulez ensuite les informations utilisateur correctes dans un objet AuthenticationInfo. Dans la méthode doCredentialsMatch () du comparateur (CredentialsMatcher), la correspondance de mot de passe (comparaison) est effectuée selon certaines règles. Si la correspondance est réussie, la connexion est réussie.

Une fois la connexion réussie, les informations de connexion du sujet seront stockées dans la session. Ensuite, la prochaine fois que vous accédez à des ressources ou des interfaces restreintes, vous n'avez pas besoin de vous reconnecter.

2. Le processus d'authentification de connexion après avoir rejoint Jwt

Y a-t-il un conflit entre l'authentification de connexion d'origine de Jwt et Shiro? En fait, Jwt est essentiellement un jeton spécial. En d'autres termes, Shiro + Jwt signifie utiliser un jeton (Jwt) pour remplacer la session d'origine de Shiro, et le serveur utilisant un jeton est plus approprié pour les projets où l'avant et l'arrière sont séparés. Peu importe si le frontal est un projet de vue ou une application Android, etc.

Quel est le processus après le remplacement? Tout d'abord, pour la première connexion, nous avons besoin que l'utilisateur saisisse le nom d'utilisateur et le mot de passe (selon la situation réelle, voici un exemple simple), puis se connecte selon le processus d'authentification de connexion d'origine de Shiro. Si la connexion réussit, le serveur renvoie une chaîne Jwt (équivalente à l'émission d'un jeton) au frontal. La prochaine fois que l'utilisateur accède aux ressources restreintes du serveur, il peut y accéder tant qu'il porte le Jwt correct et légal.

Quelle est l'idée principale de réalisation? Nous devons implémenter un filtre pour intercepter toutes les requêtes. Pour le traitement spécial des requêtes avec Jwt dans l'en-tête de requête, nous n'utilisons pas le processus de connexion par défaut de Shiro pour traiter, mais utilisons notre processus de traitement personnalisé, y compris l'implémentation du jeton correspondant, Realm , CredentialsMatcher. Une fois l'authentification passée, la demande est transmise à la couche Controller.

 Après une analyse, cette démo est divisée en trois parties: enregistrement , connexion par mot de passe et accès avec jeton (jwt) . Parmi eux, la partie "carry token (jwt) access" est la logique fondamentale de Shiro d'intégration de Jwt.

Analyse du code de base

One, importation de dépendances et table de base de données

Je ne publierai pas d'autres dépendances telles que les dépendances liées aux bases de données et autres. Pour plus de détails, reportez-vous au code source Github de la démo: 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. Inscription

Lors de l'enregistrement, nous devons crypter le mot de passe en clair saisi par l'utilisateur via Md5Hash, et transmettre le sel aléatoire crypté (différent pour chaque utilisateur, il doit être stocké dans la table utilisateur) et le nombre de hachages (le même pour chaque utilisateur).

Ensuite, lorsque vous utilisez un mot de passe pour vous connecter, nous devons crypter le mot de passe saisi par l'utilisateur avec les mêmes règles de cryptage, puis le comparer avec le mot de passe crypté stocké dans la base de données lors de l'inscription. S'il en est de même, la connexion est réussie.

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

Troisièmement, connexion par mot de passe

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: domaine qui gère la connexion par mot de passe, écrase la méthode 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;
    }
}

Le processus principal de connexion par mot de passe est fondamentalement tellement. Cependant, vous devez également écrire une classe de configuration pour enregistrer LoginRealm dans Shiro, et cela affichera la classe de configuration de Shiro uniformément. Le code ci-dessus implique également deux classes d'outils:

ApplicationContextUtil: pour un accès plus flexible aux composants dans le conteneur IOC

JwtUtil: La classe d'outils de jwt, qui fournit plusieurs méthodes statiques principales, notamment: générer jwt, vérifier jwt, obtenir des données dans jwt et obtenir l'heure d'émission 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;
    }
}

Quatre, portez l'accès au jwt

JwtToken: analogie avec 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: filtre de demande global. Rejetez les demandes qui ne portent pas de jetons et ne peuvent pas accéder aux ressources restreintes. Pour les demandes avec des jetons, vérifiez que les jetons sont valides et relisez-les plus efficacement sur la couche contrôleur

/**
 * @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: le comparateur 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);
    }
}

Five, la classe de configuration personnalisée de Shiro

En résumé, cette classe de configuration fait principalement deux choses:

1. Enregistrez le royaume, le filtre, etc. que nous avons définis ci-dessus sur Shiro

2. Définissez la règle d'interception des demandes

Pour plus de détails sur l'analyse du code, consultez les commentaires. Trouble obsessionnel-compulsif personnel, les commentaires sont assez acceptables.

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

}

Le blog se concentre sur l'analyse logique des idées. Les étudiants dans le besoin peuvent se rendre sur Github pour récupérer le code source. https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt

S'il vous plaît aime et favoris. . . muah

Je suppose que tu aimes

Origine blog.csdn.net/qq_43290318/article/details/108225519
conseillé
Classement