SpringBoot + Shiro + Jwt로 로그인 인증, 코드 분석 달성

머리말

Shiro 프레임 워크를 이해하는 데 며칠이 걸렸고 (너무 심층적이지 않음) 온라인 정보를 기반으로 데모를 만들었습니다. SpringBoot 2.2.9.RELEASE + Shiro + Jwt로 로그인 인증을 달성했습니다.

1.이 데모는 로그인 인증 (보다 일반적인)에 중점을두고 있으며 권한 제어는 특정 비즈니스에 맞게 설계 되었기 때문에 기본적으로 권한 제어를 포함하지 않습니다. 따라서 본 데모는 실제 상황에 따라 약간의 수정이 가능하며 프론트 엔드 및 백엔드 분리 프로젝트의 로그인 모듈로 사용할 수 있습니다.

2.이 블로그는 Shiro와 Jwt에 대한 이해가있는 분들에게 적합합니다. 최소한 몇 가지 주요 클래스의 기능과 방법에 대한 이해가있는 분들에게 적합합니다. 아래에서는 확장하지 않고 해당 개념과 코드 만 분석하겠습니다. 데모.

사고 분석

1. Shiro의 원래 로그인 인증 프로세스

subject.login (new UsernamePasswordToken (username, password)); 이것은 로그인 인증을 수행하기위한 키 코드이며 Shiro는 doGetAuthenticationInfo ()의 사용자 이름을 기반으로 데이터베이스에서 올바른 사용자 정보 (암호 정보 포함)를 찾습니다 . 대응 영역, 암호)는 암호화되어 저장 될 수있다. 그런 다음 올바른 사용자 정보를 AuthenticationInfo 개체에 캡슐화합니다. 비교기 (CredentialsMatcher)의 doCredentialsMatch () 메소드에서는 특정 규칙에 따라 비밀번호 일치 (비교)가 수행됩니다. 일치가 성공하면 로그인이 성공한 것입니다.

로그인이 성공하면 주체의 로그인 정보가 세션에 저장됩니다. 그러면 다음에 제한된 리소스 또는 인터페이스에 액세스 할 때 다시 로그인 할 필요가 없습니다.

2. Jwt 가입 후 로그인 인증 과정

Jwt와 Shiro의 원래 로그인 인증간에 충돌이 있습니까? 사실 Jwt는 본질적으로 특별한 토큰입니다. 즉, Shiro + Jwt는 토큰 (Jwt)을 사용하여 Shiro의 원래 세션을 대체하는 것을 의미하며, 토큰을 사용하는 서버는 프런트 엔드와 백 엔드가 분리 된 프로젝트에 더 적합합니다. 프런트 엔드가 vue 프로젝트이든 Android 앱이든 상관 없습니다.

교체 후 절차는 어떻게됩니까? 먼저 첫 번째 로그인을 위해 사용자 이름과 암호를 입력 한 다음 (실제 상황에 따라 간단한 예가 있습니다.) Shiro의 원래 로그인 인증 프로세스에 따라 로그인해야합니다. 로그인에 성공하면 서버는 프런트 엔드에 Jwt 문자열 (토큰 발급과 동일)을 반환합니다. 다음에 사용자가 서버의 제한된 리소스에 액세스 할 때 올 바르고 합법적 인 Jwt를 가지고있는 한 액세스 할 수 있습니다.

실현의 주요 아이디어는 무엇입니까? 모든 요청을 가로채는 필터를 구현해야합니다. 요청 헤더에 Jwt를 사용하여 요청을 특수 처리하려면 Shiro의 기본 로그인 프로세스를 사용하지 않고 해당 토큰, Realm 구현을 포함한 사용자 지정 처리 프로세스를 사용합니다. , CredentialsMatcher. 인증이 통과되면 요청이 컨트롤러 계층으로 해제됩니다.

 분석 후이 데모는 등록 , 비밀번호 로그인토큰으로 액세스 (jwt)의 세 부분으로 나뉩니다 . 그중에서 "캐리 토큰 (jwt) 액세스"부분은 Jwt를 통합하는 Shiro의 핵심 논리입니다.

핵심 코드 분석

하나, 종속성 가져 오기 및 데이터베이스 테이블

데이터베이스 관련 및 기타 종속성과 같은 다른 종속성은 게시하지 않겠습니다. 자세한 내용은 데모의 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>

2. 등록

등록시 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 : 암호 로그인을 처리하는 영역, 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;
    }
}

암호 로그인의 주요 프로세스는 기본적으로 너무 많습니다. 그러나 Shiro에서 LoginRealm을 등록하기위한 구성 클래스도 작성해야하며, 그러면 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 : 전역 요청 필터. 토큰을 전달하지 않고 제한된 리소스에 액세스 할 수없는 요청을 거부합니다. 토큰이있는 요청의 경우 토큰이 유효한지 확인하고 가장 효과적으로 컨트롤러 계층에 재생합니다.

/**
 * @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