赞
踩
目录
3. SpringSecurity + JWT 实现登录认证
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)
头部(Header):包含了关于生成该 JWT 的信息以及所使用的算法类型。
载荷(Payload):包含了要传递的数据,例如身份信息和其他附属数据。
签名(Signature):使用密钥对头部和载荷进行签名,以验证其完整性。
JWT 相较于传统的 Session 认证机制,具有以下优势:
无需服务器存储状态:传统的基于 Session 认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。
跨域支持:由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。
适应微服务架构:在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
自包含:JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
扩展性:JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。
总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。
JWT 工作原理包含三部分:
1. 生成 JWT
2. 传输 JWT
3. 验证 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";
- }
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,
- });
- }
- @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();
- }
- }

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

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

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

得到当前登录对象后,想要获取当前登录对象的 uid,就可以通过如下代码来获取:
Long uid = ObjectUtil.isNotNull(userDetails) ? userDetails.getUid() : NumberUtils.LONG_ZERO;
使用 ThreadLocal 可以更加简单的存取当前用户的 id,并且客户端的一次请求用的是同一个线程 id,多次请求线程 id 也不同,也不会出现线程安全问题。
① 创建 ThreadLocal 工具类 BaseContext
- public class BaseContext {
-
- public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
-
- public static void setCurrentId(Long id) {
- threadLocal.set(id);
- }
-
- public static Long getCurrentId() {
- return threadLocal.get();
- }
-
- public static void removeCurrentId() {
- threadLocal.remove();
- }
-
- }

② 在登录认证过滤器,令牌通过后,存储 uid
- @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();
-
- // --------- 使用 ThreadLocal 存储当前用户 ID ---------
- BaseContext.setCurrentId(uid);
- }
- }
- }
- filterChain.doFilter(request, response);
- }
- }

③ 在需要使用 uid 的地方获取 uid
Long uid = BaseContext.getCurrentId(); // 使用 ThreadLocal 获取身份 id
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。