java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot3.0集成Jwt实现token验证

SpringBoot3.0集成Jwt实现token验证过程

作者:爱写代码的陌

文章介绍了如何在Spring Boot项目中实现JWT(JSON Web Token)的认证与授权,主要内容包括引入依赖、创建工具类、添加拦截器进行token校验、登录接口生成token、定义UserProvider解析用户信息、自定义异常处理以及如何更新token的Audience以避免token泄露

一、pom.xml引入依赖

<dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
</dependency>

二、创建工具类JwtUtils

public class JwtUtils {
    private static final HashMap<String, String> _audiences = new HashMap<>();

    public static String createJWT(UserProviderDto user) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        HashMap<String, Object> claims = new HashMap<>();
        claims.put(Const.JWT_CLAIMS_USER, JSON.toJSONString(user));

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setSubject(user.getUsername())
                .setAudience(updateAudience(user.getUsername()))
                .setIssuedAt(new Date())
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, Const.JWT_SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(TimeUtils.getTodayEnd());

        return builder.compact();
    }

    /**
     * Token解密
     */
    public static Claims parseJWT(String token) {
        try {
            // 得到DefaultJwtParser
            return Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(Const.JWT_SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                    // 设置需要解析的jwt
                    .parseClaimsJws(token)
                    .getBody();
        }
        catch (Exception ex) {
            return null;
        }
    }

    /**
     * 更新当前登录账号的最新状态
     * @param userNo 用户名
     * @return 状态标志
     */
    public static String updateAudience(String userNo) {
        if (LogwingStringUtils.isNullOrEmpty(userNo))
            return "";

        var audience = userNo + TimeUtils.getCurrentTimeStamp();
        _audiences.put(userNo, audience);

        return audience;
    }

    /**
     * 判断当前token是否最新的,不是需重新登录
     * @return boolean
     */
    public static boolean isNewestAudience(String userNo, String audience) {
        if (LogwingStringUtils.isNullOrEmpty(userNo) || LogwingStringUtils.isNullOrEmpty(audience))
            return false;

        if (!_audiences.containsKey(userNo))
            return false;

        return _audiences.get(userNo).equals(audience);
    }

    /**
     * 获取token
     * @param request 当前http请求
     * @return token
     */
    public static String getToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (LogwingStringUtils.isNullOrEmpty(token)) {
            return "";
        }

        token = token.replace("Bearer ", "");
        if (LogwingStringUtils.isNullOrEmpty(token)) {
            return "";
        }

        return token;
    }
}

三、添加全局拦截器,校验token是否有效

public class JwtInterceptor implements HandlerInterceptor {
    //主要功能拦截请求验证token
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = JwtUtils.getToken(request);
        if (LogwingStringUtils.isNullOrEmpty(token)) {
            throw new InvalidUserException();
        }

        var claims = JwtUtils.parseJWT(token);
        if (claims == null) {
            throw new InvalidUserException();
        }

        Date createTime = claims.getIssuedAt();
        if (createTime == null || createTime.compareTo(TimeUtils.getTodayStart()) < 0) {
            throw new InvalidUserException();
        }

        String audience = claims.getAudience();
        if (!JwtUtils.isNewestAudience(claims.getSubject(), audience)) {
            throw new InvalidUserException();
        }

        return true;
    }
}

四、将拦截器添加到WebConfig配置中

拦截请求进行token校验

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                //拦截的路径
                .addPathPatterns("/**")
                //不需要的拦截请求
                .excludePathPatterns(
                        "/api/OAuth/login",
                        "/api/OAuth/logout",
                        "/api/getDate",
                        "/swagger-ui/**",
                        "/v3/**");
    }
}

五、登录接口增加生成token

返回前端,账号密码校验完成之后生成token,将用户基本信息存入token

public LoginOutputDto login(LoginUserInputDto input) {
        Admin loginUser = adminRepository.getLoginUserByUserName(input.getUserName());
        if (loginUser == null)
            return new LoginOutputDto(false, "用户名不存在,请重新输入。");

        var inputPassword = RSAUtils.decryptByRSA(input.getEncryptedPassword());
        var encryptPassword = MD5Utils.encryptMD5(MD5Utils.encryptMD5(inputPassword) + loginUser.getSalt());
        if (!encryptPassword.equals(loginUser.getPassword()))
            return new LoginOutputDto(false, "密码有误,请重新输入。");

        UserProviderDto userProvider = getUserProvider(loginUser);
        String token = JwtUtils.createJWT(userProvider);
        LoginOutputDto output = new LoginOutputDto(true, "登录成功。");
        output.setAccessToken(token);

        return output;
    }

    private UserProviderDto getUserProvider(Admin loginUser) {
        UserProviderDto userProvider = new UserProviderDto();
        userProvider.setId(loginUser.getId());
        userProvider.setUsername(loginUser.getUsername());
        userProvider.setStatus(loginUser.getStatus());
        userProvider.setType(loginUser.getType());

        return userProvider;
    }
}

六、定义UserProvider

提供从token解析出当前用户信息的接口方法

@Service
public class UserProvider implements IUserProvider {
    @Autowired
    private HttpServletRequest httpServletRequest;

    @Override
    public UserProviderDto getCurrentUser() {
        String token = JwtUtils.getToken(httpServletRequest);
        if (LogwingStringUtils.isNullOrEmpty(token)) {
            throw new RuntimeException(Const.UN_LOGIN_TIPS);
        }

        Claims claims = JwtUtils.parseJWT(token);
        if (claims == null) {
            throw new RuntimeException(Const.UN_LOGIN_TIPS);
        }

        Date createTime = claims.getIssuedAt();
        if (createTime == null || createTime.compareTo(TimeUtils.getTodayStart()) < 0) {
            throw new RuntimeException(Const.UN_LOGIN_TIPS);
        }

        String user = (String) claims.get(Const.JWT_CLAIMS_USER);

        return JSON.parseObject(user, UserProviderDto.class);
    }
}

七、定义controller接口

登录接口

@PostMapping("/api/OAuth/login")
@ResponseBody
@Operation(summary = "登录授权接口", description = "登录授权接口")
public LoginOutputDto login(@Valid @RequestBody LoginUserInputDto input) throws NoSuchAlgorithmException {
    return loginService.login(input);
}

退出接口

退出时更新当前账号的Audience,用来避免退出后用旧的token仍然可以通过接口token是否有效的拦截(因为jwt具有无状态性,生成token之后改token只能等设置的过期时间到了之后才能自动销毁,否则该token在过期时间之前仍然有效),具体方法见jwtutils中的updateaudience和isnewestaudience方法

@PostMapping("/api/OAuth/logout")
@ResponseBody
@Operation(summary = "退出接口")
public ResponseOutputDto logout() {
    var user = userProvider.getCurrentUser();
    JwtUtils.updateAudience(user.getUsername());

    return new ResponseOutputDto();
}

八、自定义异常InvalidUserException

并拦截全局异常InvalidUserException,捕捉拦截器抛出的异常,返回给前端

public class InvalidUserException extends Exception {
    public InvalidUserException() {
        super(Const.UN_LOGIN_TIPS);
    }
}
@ControllerAdvice
@Slf4j
public class SystemExceptionHandler {
    @ExceptionHandler(InvalidUserException.class)
    @ResponseBody
    public LoginOutputDto doException(InvalidUserException ex) {
        return new LoginOutputDto(false, ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseOutputDto doException(Exception ex) {
        if (ex.getMessage().contains("未登录")) {
            return new ResponseOutputDto(false, ex.getMessage());
        }

        log.error(ex.getMessage(), ex);
        return new ResponseOutputDto(false, "程序发生错误,请联系客服。");
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文