基于SpringBoot3+Security6+JWT实现鉴权机制

本文介绍了基于JWT的鉴权机制,包括JWT的三部分组成:头部、载荷和签名,以及如何在SpringBoot3和Security6中新建相关工具类和改造login接口以实现鉴权。通过登录接口的演示,展示了JWT的使用和验证过程。
1. JWT简介

为什么要说下呢,JWT三部分组成,就要刚刚笔者参加的2023下半年系统架构师考试中考到了,然后我竟然想不起来了。。。

JWT(JSON Web Token)由三个部分组成,它们分别是头部(header)、载荷(payload)和签名(signature)

  • 头部(Header):JWT的头部是一个包含两个部分的JSON对象,用于描述签名算法和令牌类型。它通常包含以下信息:
    typ(类型):令牌的类型,这里通常是"JWT"。
    alg(算法):用于签名令牌的算法,例如HMAC、RSA或者其他加密算法。
  • 载荷(Payload):JWT的载荷部分是存储实际数据的地方,它包含了一系列声明,也是一个JSON对象。载荷可以包含一些预定义的声明(例如,iss(发行人)、exp(过期时间)、sub(主题)等),以及自定义的声明。这些声明提供了有关令牌的信息,但并没有进行加密。
  • 签名(Signature):JWT的签名部分用于验证令牌的真实性和完整性。签名是通过将编码后的头部和载荷与一个密钥进行加密生成的。在验证JWT时,接收方可以使用相同的密钥进行加密,并通过比较签名来确保令牌未被篡改。

签名的生成方式取决于在头部指定的算法。常见的签名算法包括HMAC(使用密钥进行哈希计算)和RSA(使用非对称加密算法)。

最终,这三个部分会用点号(.)连接起来形成一个完整的JWT。例如:头部.Base64编码的头部 + “.” + 载荷.Base64编码的载荷 + “.” + 签名。

2. 新建RequestUtils.java
public class RequestUtils {

    /**
     * 获取上下文Request
     *
     * @return
     */
    public static HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    /**
     * 获取上下文Response
     *
     * @return
     */
    public static HttpServletResponse getResponse() {
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
    }
}
3. 新建Claims.java类
@Data
public class Claims {

    /**
     * 过期时间
     */
    private Long exp;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 用户中文名
     */
    private String username;

    /**
     * 用户登录账号
     */
    private String account;

    /**
     * 机构编码
     */
    private String orgCode;

    /**
     * 状态 0启用 1禁用
     */
    private String state;

    /**
     * 用户角色
     */
    private List<String> roles;

    /**
     * 权限列表
     */
    private List<String> permissions;

    public Claims() {

    }

    public Claims(Long exp, String username, String account, String state, List<String> roles, List<String> permissions) {
        this.exp = exp;
        this.username = username;
        this.account = account;
        this.state = state;
        this.roles = roles;
        this.permissions = permissions;
    }
}
4.新建TokenUtils.java类
public class TokenUtils {

    private static final String TOKEN_SING = "Shenjian@Suanfaxiaosheng";
    // 过期时间15天
    private static final Long EXPIRATION_TIME = 86400000 * 15L;

    /**
     * 生成Token
     */
    public static String buildToken(Claims claims) {
        try {
            // 对密钥进行签名
            byte[] bytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
            JWSSigner jwsSigner = new MACSigner(Arrays.copyOf(bytes, 128));
            // 准备JWS header
            JWSHeader jwsHeader = new JWSHeader
                    .Builder(JWSAlgorithm.HS512)
                    .type(JOSEObjectType.JWT)
                    .build();

            claims.setExp(new Date(System.currentTimeMillis() + EXPIRATION_TIME).getTime());

            Payload payload = new Payload(JSON.toJSONString(claims));
            // 封装JWS对象
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);
            // 签名
            jwsObject.sign(jwsSigner);
            return "Bearer " + jwsObject.serialize();
        } catch (KeyLengthException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 验证token
     *
     * @param token
     * @return
     */
    public static boolean validateToken(String token) {
        JWSObject jwsObject;
        try {
            token = token.replace("Bearer ", "");
            jwsObject = JWSObject.parse(token);
            // HMAC验证器
            byte[] decodedBytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
            JWSVerifier jwsVerifier = new MACVerifier(Arrays.copyOf(decodedBytes, 128));
            if (!jwsObject.verify(jwsVerifier)) {
                return false;
            }

            String payload = jwsObject.getPayload().toString();
            Claims claims = JSON.parseObject(payload, Claims.class);
            if (claims.getExp() < new Date().getTime()) {
                return false;
            }
            return true;
        } catch (ParseException | JOSEException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 从token中获取用户ID
     *
     * @return
     */
    public static String getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getUserId();
    }

    public static Claims getClaimsFromToken() {
        String token = RequestUtils.getRequest().getHeader("Authorization");
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return parseToken(token);
    }

    /**
     * 解析token
     *
     * @return
     */
    private static Claims parseToken(String token) {
        JWSObject jwsObject;
        try {
            token = token.replace("Bearer ", "");
            jwsObject = JWSObject.parse(token);
            // HMAC验证器
            byte[] decodedBytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
            JWSVerifier jwsVerifier = new MACVerifier(Arrays.copyOf(decodedBytes, 128));
            if (!jwsObject.verify(jwsVerifier)) {
                return null;
            }

            String payload = jwsObject.getPayload().toString();
            Claims claims = JSON.parseObject(payload, Claims.class);
            return claims;
        } catch (ParseException | JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        System.out.println(86400000 / 1000 / 60 / 60);
    }
}
5.改造login接口
@Service
public class UserServiceImpl implements UserService {

    private UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public ResponseVo login(UserDto userDto) {
        // 根据用户登录名获取用户实体
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", userDto.getUsername());
        wrapper.last("LIMIT 1");
        User user = userMapper.selectOne(wrapper);

        // 假设用户一定存在且密码正确

        Claims claims = new Claims();
        claims.setUserId(user.getId());
        claims.setUsername(user.getUsername());

        // 获取权限列表
        String token = TokenUtils.buildToken(claims);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("token", token);
        return ResponseVo.success(jsonObject);
    }
}
6.效果验证

访问http://localhost:8080/springdoc/swagger-ui/index.html#/%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/login

输入json串{"username": "sfxs","password": 1111}即可成功返回Token


后半文我们将实践前后端访问Token鉴权,后端校验Token的完整代码

1. build.gradle新增依赖包
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 匿名用户无权访问控制
@Slf4j
@Component
public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 允许跨域
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 允许自定义请求头token(允许head跨域)
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Role, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        response.getWriter().print(JSON.toJSONString(ResponseVo.message(ResponseCode.UN_AUTHORIZED)));
    }
3. 访问拒绝处理器
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.getWriter().write(JSON.toJSONString(ResponseVo.message(ResponseCode.LICENSE_EXPIRED)));
    }
}
4. 基于数据库登录验证
/**
 * 通用实体与DTO互相转换工具类
 */
public class CommonDtoUtils {


    public static <T> T transform(Object source, Class<T> targetClass) {
        if (source == null) {
            return null;
        }
        try {
            String jsonSource = JSON.toJSONString(source);
            return JSONObject.parseObject(jsonSource, targetClass);
        } catch (Exception ex) {
            throw ex;
        }
    }

    public static <T> List<T> transformList(List<?> listSource, Class<T> targetClass) {
        if (listSource == null) {
            return null;
        }
        try {
            String jsonSource = JSON.toJSONString(listSource);
            return JSONArray.parseArray(jsonSource, targetClass);
        } catch (Exception ex) {
            throw ex;
        }
    }
}
@Data
public class LoginUserDto implements UserDetails, CredentialsContainer {

    /**
     * 登录账号
     */
    private String username;

    /**
     * 认证完成后,擦除密码等信息
     */
    @Override
    public void eraseCredentials() {}

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     */
    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    /**
     * 用户是否被启用或禁用。禁用的用户无法进行身份验证。
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Slf4j
@Component
public class DefaultUserDetailsServiceImpl implements UserDetailsService {

    private UserMapper userMapper;

    @Autowired
    private DefaultUserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("id", userId);
        User userInfo = userMapper.selectOne(queryWrapper);
        if (userInfo == null) {
            log.info("登录用户账户:{} 不存在", userId);
            throw new UsernameNotFoundException("登录用户:" + userId + " 不存在");
        }
        LoginUserDto loginUserDto = CommonDtoUtils.transform(userInfo, LoginUserDto.class);
        return loginUserDto;
    }
}
5. 新增Token过滤器
@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Resource
    private UserDetailsService userDetailsService;

    private static final Set<String> ignoreUrlSet = new HashSet<>();

    static {
        ignoreUrlSet.add("/login");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        String token = RequestUtils.getRequest().getHeader("Authorization");
        // 过滤登录页面,防止token失效
        if (StringUtils.isNotBlank(token) && !"null".equals(token) && !ignoreUrlSet.contains(request.getRequestURI())) {
            String userId = TokenUtils.getUserIdFromToken(token);
            if (StringUtils.isNotBlank(userId) && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
                if (TokenUtils.validateToken(token)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                } else {
                    response.setHeader("Content-type", "application/json;charset=UTF-8");
                    response.getWriter().print(JSON.toJSONString(ResponseVo.message(ResponseCode.TOKEN_EXPIRATION)));
                    return ;
                }
            }

            // 权限过滤器,只加载当前菜单下权限
            buildCurrentUserRoleByMenuCode(request, response);
        }
        filterChain.doFilter(request, response);
    }

    /**
     * 获取当前用户当前菜单权限
     *
     * @param request
     * @return
     */
    private void buildCurrentUserRoleByMenuCode(HttpServletRequest request, HttpServletResponse response) {

    }
}
6. 新增Spring Security 配置,建造者模式
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    // 匿名登录处理
    private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint;
    private MyAccessDeniedHandler myAccessDeniedHandler;
    private JwtTokenFilter jwtTokenFilter;

    @Autowired
    public SecurityConfiguration(AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint, MyAccessDeniedHandler myAccessDeniedHandler
            , JwtTokenFilter jwtTokenFilter) {
        this.anonymousAuthenticationEntryPoint = anonymousAuthenticationEntryPoint;
        this.myAccessDeniedHandler = myAccessDeniedHandler;
        this.jwtTokenFilter = jwtTokenFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors(withDefaults()).csrf((csrf) -> csrf.disable())
                // 因为使用JWT,所以不需要HttpSession
                .sessionManagement((sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)))
                .authorizeHttpRequests((authz) -> authz
                        // OPTIONS请求全部放行
                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                        // 放行接口
                        .requestMatchers("/login").permitAll()
                        .requestMatchers("/register").permitAll()
                        .requestMatchers("/verifyCode").permitAll()
                        .requestMatchers("/user/resetPassword").permitAll()
                        // 放行Swagger页面
                        .requestMatchers("/springdoc/**").permitAll()
                        // 所有请求全部需要鉴权认证
                        .anyRequest().authenticated()
                )
                // 异常处理(权限拒绝、登录失效等)
                .headers((headers) -> headers.frameOptions(frameOptionsConfig -> frameOptionsConfig.disable()))
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(anonymousAuthenticationEntryPoint)
                        .accessDeniedHandler(myAccessDeniedHandler)
                );

        // 使用自定义的 Token过滤器 验证请求的Token是否合法
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.headers(headers -> headers.cacheControl(cacheControlConfig -> {}));
        return http.build();
    }
}
7. 验证效果
@FeignClient(value = "cloud", contextId = "cloud")
@Component
public interface CloudClient {

    @PostMapping(value = "/testToken", produces = MediaType.APPLICATION_JSON_VALUE)
    @Operation(summary = "测试Token", tags = "用户管理", security = { @SecurityRequirement(name = "token")})
    ResponseVo testToken(@RequestBody UserDto userDto);

}
@RestController
public class CloudController implements CloudClient {

    @Override
    public ResponseVo testToken(UserDto userDto) {
        return ResponseVo.success("TOKEN测试成功");
    }
}

应用启动后,我们在Swagger页面首先登录获取Token,然后设置Token,访问testToken接口,效果如下


欢迎关注公众号算法小生,更多原创等你来

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法小生Đ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值