目录
3. SpringSecurity + JWT 实现登录认证
1. JWT 的组成和优势
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)
头部(Header):包含了关于生成该 JWT 的信息以及所使用的算法类型。
载荷(Payload):包含了要传递的数据,例如身份信息和其他附属数据。
签名(Signature):使用密钥对头部和载荷进行签名,以验证其完整性。
JWT 相较于传统的 Session 认证机制,具有以下优势:
无需服务器存储状态:传统的基于 Session 认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。
跨域支持:由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。
适应微服务架构:在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
自包含:JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
扩展性:JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。
总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。
2. JWT 的工作原理
JWT 工作原理包含三部分:
1. 生成 JWT
2. 传输 JWT
3. 验证 JWT
2.1 生成 JWT
在用户登录时,当服务器端验证了用户名和密码的正确性后,会根据用户的信息,如用户 ID 和用户名称,加上服务器端存储的 JWT 秘钥一起来生成一个 JWT 字符串,也就是我们所说的 Token.
@Value("${jwt.secret}") private String jwtSecret; /** * 用户登录 * * @return {@link ResponseEntity } */ @PostMapping("/login") public ResponseEntity login(@Validated UserDto userDto, HttpServletRequest request) { // --- 验证图片验证码 --- // 验证用户名密码 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, userDto.getUsername()); User user = userService.getOne(queryWrapper); if (user != null && passwordEncoder.matches(userDto.getPassword(), user.getPassword())) { // 生成 JWT HashMap<String, Object> payLoad = new HashMap<>() {{ put(JWTConstant.JWT_UID_KEY, user.getUid()); put(JWTConstant.JWT_USERNAME_KEY, user.getUsername()); }}; HashMap<String, String> result = new HashMap<>(); result.put(JWTConstant.JWT_KEY, JWTUtil.createToken(payLoad, jwtSecret.getBytes())); result.put(JWTConstant.JWT_USERNAME_KEY, user.getUsername()); return ResponseEntity.success(result); } return ResponseEntity.fail("用户名或密码不正确!"); }
jwt: secret: aicloud_springcloud_secret
public class JWTConstant { public static final String JWT_KEY = "jwt"; public static final String JWT_UID_KEY = "uid"; public static final String JWT_USERNAME_KEY = "username"; public static final String JWT_TOKEN_KEY = "Authorization"; }
2.2 传输JWT
JWT 通常存储在客户端的 Cookie、LocalStorage、SessionStorage 等位置,客户端在每次请求时把 JWT 放在 Header 请求头中传递给服务器端.
存储 JWT 到 LocalStorage
$.ajax({ url: '/user/login', type: 'POST', data: field, success: function (res) { if (res.code === 200) { layui.data(localStorage_jwt_key, { // 将生成的 jwt 存储到 LocalStorage key: jwt_token_key, value: res.data.jwt, }); layui.data(localStorage_username_key, { // 存储 username key: login_username_key, value: res.data.username, }); // 刷新父页面 (登录页是个弹窗) window.parent.location.href = window.parent.location.href } else { layer.msg("登录失败:" + res.msg); } } });
var localStorage_username_key = "login_username_key" var localStorage_jwt_key = "jwt_token_key" var login_username_key = "username" var jwt_token_key = "authorization"
登录成功后,其他请求的 header 中带上 token
function authAjax($, url, data, callback) { $.ajax({ url: url, type: 'POST', headers: { 'Authorization': layui.data("jwt_token_key").authorization }, data: data, success: callback, }); }
3. SpringSecurity + JWT 实现登录认证
3.1 配置 Spring Security 安全过滤链
@Configuration @EnableWebSecurity public class SecurityConfig { @Resource private LoginAuthenticationFilter loginAuthenticationFilter; /** * 加盐加密 * * @return {@link PasswordEncoder } */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置 Spring Security 安全过滤链 * * @param http * @return {@link SecurityFilterChain } * @throws Exception */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // 禁用明文验证 .httpBasic(AbstractHttpConfigurer::disable) // 禁用 CSRF 验证 .csrf(AbstractHttpConfigurer::disable) // 禁用默认登录页 .formLogin(AbstractHttpConfigurer::disable) // 禁用默认 header,支持 iframe 访问页面 .headers(AbstractHttpConfigurer::disable) // 禁用默认注销页 .logout(AbstractHttpConfigurer::disable) // 禁用 Session (默认使用 JWT 认证) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth // 允许访问的资源 .requestMatchers( "/layui/**", "/js/**", "/image/**", "/login.html", "/index.html", "/reg.html", "/user/login", "/user/reg", "/captcha/create", "/discuss/delete", "/discuss/detail", "/kafka/**", "/swagger-ui/**", "/v3/**", "/doc.html", "/webjars/**" ).permitAll() // 其他请求都需要认证拦截 .anyRequest().authenticated() ) // 添加自定义认证过滤器 .addFilterBefore(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }
3.2 自定义登录认证过滤器
@Component public class LoginAuthenticationFilter extends OncePerRequestFilter { @Value("${jwt.secret}") private String jwtSecret; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1.获取 JWT 令牌 String token = request.getHeader(JWTConstant.JWT_TOKEN_KEY); if (!StringUtils.isBlank(token)) { // 2.判断 JWT 令牌正确性 if (JWTUtil.verify(token, jwtSecret.getBytes())) { // 3.获取用户信息,存储 Security 中 JWT jwt = JWTUtil.parseToken(token); if (ObjectUtil.isNotNull(jwt) && jwt.getPayload(JWTConstant.JWT_UID_KEY) != null && jwt.getPayload(JWTConstant.JWT_USERNAME_KEY) != null) { Long uid = Long.parseLong(jwt.getPayload(JWTConstant.JWT_UID_KEY).toString()); String username = jwt.getPayload(JWTConstant.JWT_USERNAME_KEY).toString(); // 4.创建用户对象 SecurityUserDetails userDetails = new SecurityUserDetails(uid, username, EMPTY); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // 绑定 request 对象 authentication.setDetails(new WebAuthenticationDetailsSource() .buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }
3.3 实现SpringSecurity用户对象
@Data @Builder public class SecurityUserDetails implements UserDetails { @Serial private static final long serialVersionUID = -829716430599304080L; private Long uid; private String username; private String password; public SecurityUserDetails(Long uid, String username, String password) { this.uid = uid; this.username = username; this.password = password; } /** * 权限 * * @return {@link Collection }<{@link ? } {@link extends } {@link GrantedAuthority }> */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } /** * 密码 * * @return {@link String } */ @Override public String getPassword() { return ""; } /** * 用户名 * * @return {@link String } */ @Override public String getUsername() { return ""; } /** * 账户是否过期 * * @return boolean */ @Override public boolean isAccountNonExpired() { return false; } /** * 账号是否锁定 * * @return boolean */ @Override public boolean isAccountNonLocked() { return false; } /** * 密码是否过期 * * @return boolean */ @Override public boolean isCredentialsNonExpired() { return false; } /** * 账号是否可用 * * @return boolean */ @Override public boolean isEnabled() { return true; } }
3.4 获取当前登录用户
public class SecurityUtil { /** * 获取当前登录用户 * * @return {@link SecurityUserDetails } */ public static SecurityUserDetails getCurrentUser() { SecurityUserDetails userDetails = null; try { userDetails = (SecurityUserDetails) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); } catch (Exception e) { } return userDetails; } }