## 任务名称: 任务7-基于JWT的用户认证实现 ### 目标: - spring的配置类的使用 - 基于jwt的认证和授权 ### 预备知识: - JWT - spring security ### 操作步骤: #### 1. 撰写登录接口的API文档 使用模板[API接口说明模板](../API接口说明模板.md),根据演示站点的实际请求和回应,填写API接口说明模板。 #### 2. 引入spring security 在build.gradle中引入spring security依赖,在 dependencies中添加如下依赖: ```groovy implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' ``` #### 3. 引入JWT工具包 jjwt 是一个轻量级的JWT工具包,用于生成和验证JWT。 项目中引入jjwt,在build.gradle中添加如下依赖: ```groovy implementation 'io.jsonwebtoken:jjwt-api:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' ``` #### 4. 系统安全相关的自定义配置项 在application.yml中配置系统安全相关配置项: ```yaml app: security: jwt: secret-key: uYuVw0mke38MfLhO19wUQyRgwrmYo89ibpQTXPHi4vg= expiration: 36000000 white-list: - path: "/api-docs/**" methods: ["GET","PUT","POST"] - path: "/swagger-ui/**" methods: ["GET"] - path: "/v3/**" methods: [ "GET" ] - path: "/v1/auth/**" methods: [ "GET","PUT","POST"] allowed-origins: - "http://localhost:5173" ``` 实现安全相关自定义配置的配置类SecurityProperties: ```java package com.lk.paopao.conf; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Getter @Setter @Component @ConfigurationProperties(prefix = "app.security") public class SecurityProperties { private List whiteList = new ArrayList<>(); private List allowedOrigins; @Getter @Setter public static class WhiteListRule { private String path; private List methods; } } ``` #### 5. 实现UserDetailsService接口和UserDetails实现类 UserDetailsService接口 UserDetailsService接口是Spring Security提供的一个核心服务接口,其主要目的是加载用户的详细信息,特别是用于认证(登录验证)和授权(访问控制)的信息。这个接口包含一个核心方法loadUserByUsername(String username),该方法根据用户名(或其他唯一的用户标识符)加载用户的详细信息。Spring Security在用户尝试登录时,会自动调用这个方法来获取用户信息,并进一步验证用户的凭证(如密码)是否正确。 实现UserDetailsService接口,需要定义一个类并实现这个方法,通常这个方法会查询数据库、LDAP或者其他用户存储系统,获取用户的详细信息,并封装到一个实现了UserDetails接口的对象中返回。这使得Spring Security可以灵活地与各种用户数据源集成。 UserDetails接口 UserDetails接口代表了用户的核心安全信息,包括但不限于用户名、密码、账户是否过期、凭证是否过期、账户是否锁定等状态信息,以及用户所具有的权限(通常通过角色来表示)。通过实现这个接口,可以定制化地定义用户实体类来保存和提供这些安全相关信息。例如,一个简单的实现可能包含用户名、密码、角色列表等字段。 当一个实现了UserDetailsService的类返回一个实现了UserDetails的对象时,Spring Security会使用这些信息来决定用户是否可以成功登录,以及登录后能够访问哪些资源。具体来说,它会验证用户提供的密码是否与存储的密码匹配,并检查用户的账户状态是否允许登录(例如,没有过期,未被锁定)。 总结来说,实现UserDetailsService是为了让Spring Security知道如何根据用户名加载用户详细信息,而实现UserDetails则是为了定义这些详细信息应该包含哪些内容,从而支撑起Spring Security的认证和授权逻辑。这两个接口的结合使用,为Spring Security提供了一个强大且灵活的用户认证和授权机制。 **实现UserDetails接口** ```java package com.lk.paopao.security; import com.lk.paopao.entity.User; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; /** * 用户详情实现类,继承自UserDetails接口,用于提供用户认证所需信息。 */ public class UserDetailsImpl implements UserDetails { private static final long serialVersionUID = 1L; @Getter private Long id; private String username; private String password; public UserDetailsImpl(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } public static UserDetailsImpl build(User user) { return new UserDetailsImpl(user.getId(), user.getUsername(), user.getPassword()); } @Override public Collection getAuthorities() { return null; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } ``` **实现UserDetailsService接口** ```java package com.lk.paopao.security; import com.lk.paopao.entity.User; import com.lk.paopao.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 自定义用户详情服务实现类,用于加载和验证用户详细信息。 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository; /** * 根据用户名加载用户详情。 * * @param username 用户名 * @return UserDetails 用户详情对象 * @throws UsernameNotFoundException 如果用户不存在,则抛出异常 */ @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 通过用户名查找用户,如果不存在则抛出异常 User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); // 构建并返回UserDetails对象 return UserDetailsImpl.build(user); } } ``` #### 6. 实现jwt的工具类 使用jjwt生成JWT Token和验证JWT Token。 ```java package com.lk.paopao.security; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; @Component public class JwtUtils { // 日志记录器 private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); // JWT密钥 @Value("${app.security.jwt.secret-key}") private String jwtSecret; // JWT过期时间(毫秒) @Value("${app.security.jwt.expiration}") private int jwtExpirationMs; /** * 生成JWT Token * * @param authentication 认证信息,包含用户详情 * @return 生成的JWT Token字符串 */ public String generateJwtToken(Authentication authentication) { // 提取用户详情 UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); // 创建JWT,设置主题、签发时间、过期时间和签名密钥 return Jwts.builder() .subject((userPrincipal.getUsername())) .issuedAt(new Date()) .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(getSigningKey()) .compact(); } /** * 获取签名密钥 * * @return 签名所用的SecretKey */ private SecretKey getSigningKey() { // 解码JWT密钥 byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); return Keys.hmacShaKeyFor(keyBytes); } /** * 从JWT Token中提取用户名 * * @param token JWT Token字符串 * @return 提取的用户名 */ public String getUserNameFromJwtToken(String token) { // 解析JWT Token并提取主题(用户名) return Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload() .getSubject(); } /** * 验证JWT Token的有效性 * * @param authToken JWT Token字符串 * @return 如果Token有效返回true,否则返回false */ public boolean validateJwtToken(String authToken) { try { // 尝试解析JWT Token,如无异常则表示Token有效 Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(authToken); return true; } catch (Exception e) { // 记录JWT签名异常 logger.debug("Invalid JWT signature: {}", e.getMessage()); } return false; } } ``` #### 7. 创建JWT拦截器 Spring框架中,拦截器(Interceptor)是一个用于在请求处理流程之前或之后执行的一系列逻辑的组件。它们可以用于日志记录、安全控制、事务管理等。 AuthTokenFilter 是一个继承自 OncePerRequestFilter 的拦截器,它的作用是检查HTTP请求中的JWT令牌,并在令牌有效的情况下,将用户认证信息设置到Spring Security的安全上下文中。 ```java package com.lk.paopao.security; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; /** * OncePerRequestFilter作为SpringMVC中的一个过滤器,在每次请求的时候都会执行。它是对于Filter的抽象实现。 * OncePerRequestFilter表示每次的Request都会进行拦截,不管是资源的请求还是数据的请求. * AuthTokenFilter类继承自OncePerRequestFilter,用于在每个HTTP请求上执行JWT令牌的验证。 */ public class AuthTokenFilter extends OncePerRequestFilter { // 注入JwtUtils服务,用于JWT令牌的处理。 @Autowired private JwtUtils jwtUtils; // 注入UserDetailsServiceImpl服务,用于根据用户名加载用户详细信息。 @Autowired private UserDetailsServiceImpl userDetailsService; // 日志记录器 private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); /** * 重写doFilterInternal方法,用于在请求链中执行过滤逻辑。 * * @param request HttpServletRequest对象,代表客户端的HTTP请求。 * @param response HttpServletResponse对象,用于向客户端发送HTTP响应。 * @param filterChain 过滤链对象,允许将请求传递给下一个过滤器或处理器。 * @throws ServletException 如果处理请求时发生Servlet相关异常。 * @throws IOException 如果处理请求时发生IO异常。 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 解析JWT令牌。 String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { // 从JWT令牌中获取用户名。 String username = jwtUtils.getUserNameFromJwtToken(jwt); // 加载用户详细信息。 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 创建认证对象。 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // 设置认证详情。 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 设置当前线程的安全上下文认证信息。 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // 记录无法设置用户认证的错误。 logger.error("Cannot set user authentication:", e); } // 继续处理请求链。 filterChain.doFilter(request, response); } /** * 从HTTP请求中解析JWT令牌。 * * @param request HttpServletRequest对象。 * @return 从请求头中解析出的JWT令牌字符串,如果无法解析则返回null。 */ private String parseJwt(HttpServletRequest request) { // 从请求头中获取"Authorization"字段。 String headerAuth = request.getHeader("Authorization"); // 如果存在且以"Bearer "开头,则提取并返回令牌字符串。 if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } } ``` #### 8. 实现 AuthenticationEntryPoint AuthenticationEntryPoint 是一个用于处理未认证请求的接口。当用户尝试访问受保护资源时,如果用户尚未通过身份验证,则AuthenticationEntryPoint将处理该请求。 ```java package com.lk.paopao.security; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 当用户请求了一个受保护的资源,但是用户没有通过认证,那么抛出异常,AuthenticationEntryPoint. Commence(..)就会被调用。 * 这个对应的代码在ExceptionTranslationFilter中,当ExceptionTranslationFilter catch到异常后,就会间接调用AuthenticationEntryPoint。 *

* AuthEntryPointJwt:处理JWT认证失败的入口点。 * 当用户请求需要认证的资源但未提供有效令牌时,会触发此入口点。 * 实现了AuthenticationEntryPoint接口,用于定义应用程序在身份验证失败时的响应行为。 */ @Component class AuthEntryPointJwt implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); /** * 当认证失败时,向客户端返回适当的错误响应。 * * @param request HttpServletRequest对象,代表客户端的HTTP请求。 * @param response HttpServletResponse对象,用于向客户端发送HTTP响应。 * @param authException 身份验证异常,包含认证失败的详细信息。 * @throws IOException 如果在写响应时发生I/O错误。 * @throws ServletException 如果在处理请求时发生Servlet异常。 */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // 记录未经授权的错误信息 logger.debug("Unauthorized error: {}", authException.getMessage()); // 设置响应类型为JSON,状态码为401(未授权) response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 构建响应体,包含状态码、错误信息和请求路径 final Map body = new HashMap<>(); body.put("code", HttpServletResponse.SC_UNAUTHORIZED); body.put("msg", authException.getMessage()); // 将响应体转换为JSON并写入响应流 final ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), body); } } ``` #### 9. spring security的配置类 需要实现一个Spring Security配置类,用于设置应用程序的安全特性,包括认证、授权、密码加密、跨域资源共享(CORS)等。 ```JAVA package com.lk.paopao.security; import com.lk.paopao.conf.SecurityProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import java.util.Map; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; @Configuration // 配置类注解,代表这是一个Spring配置类。 @EnableWebSecurity // 启用Spring Web安全功能。 @EnableMethodSecurity // 启用方法级别的安全控制。 public class SecurityConfiguration { @Autowired UserDetailsService userDetailsService; @Autowired private AuthEntryPointJwt unauthorizedHandler; @Autowired SecurityProperties securityProperties; public Map METHODS = Map.of( "GET", HttpMethod.GET, "POST", HttpMethod.POST, "PUT", HttpMethod.PUT, "DELETE", HttpMethod.DELETE ); /** * 创建并返回一个AuthTokenFilter实例,用于过滤和处理JWT认证令牌。 * * @return AuthTokenFilter 实例。https://i… */ @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } /** * 创建并配置DaoAuthenticationProvider,用于处理用户认证。 * * @return 配置好的DaoAuthenticationProvider实例。 */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } /** * 创建并返回一个BCryptPasswordEncoder实例,用于密码加密。 * * @return BCryptPasswordEncoder 实例。 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置并返回一个AuthenticationManager实例,它是Spring Security的认证管理器。 * * @param authConfig 认证配置。 * @return 配置好的AuthenticationManager实例。 * @throws Exception 可能抛出的异常。 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } /** * 配置HTTP安全设置,定义如何处理请求和授权。 * * @param http HttpSecurity配置对象。 * @param introspector 用于检查HandlerMapping的 introspector。 * @return 配置好的SecurityFilterChain,它定义了应用的过滤链。 * @throws Exception 可能抛出的异常。 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { // 禁用CSRF保护 http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(req -> { // 允许OPTIONS请求 req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll(); // 配置白名单 for (SecurityProperties.WhiteListRule rule : securityProperties.getWhiteList()) { for (String method : rule.getMethods()) { req.requestMatchers(METHODS.get(method.toUpperCase()), rule.getPath()).permitAll(); } } // 其他请求需要身份验证 req.anyRequest().authenticated(); }); // 设置未授权访问的处理,设置会话管理策略为无状态.设置认证提供者,在UsernamePasswordAuthenticationFilter之前添加自定义过滤器 http.exceptionHandling(ex -> ex.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider()) .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } /** * 配置跨域过滤器 * * @return CorsFilter */ @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedHeader("*"); config.addAllowedMethod("*"); for (String origin : securityProperties.getAllowedOrigins()) { config.addAllowedOrigin(origin); } config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } } ``` #### 10. 实现登录接口 **在AuthController中添加登录接口** ```java @PostMapping("/login") public DataResult> login(@RequestBody LoginRequest loginRequest){ return ResultUtil.ok(authService.login(loginRequest.getUsername(),loginRequest.getPassword())); } ``` **在AuthService中添加登录逻辑** 并修改注册逻辑,密码加密. 完整的AuthService类如下: ```java package com.lk.paopao.service; import com.lk.paopao.dto.rest.request.RegisterRequest; import com.lk.paopao.entity.User; import com.lk.paopao.exception.ResourceExistedException; import com.lk.paopao.repository.UserRepository; import com.lk.paopao.security.JwtUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map; @Service @Transactional public class AuthService { // 从配置文件中读取默认头像地址 @Value("${app.default.head-icon}") private String DEFAULT_HEAD_ICON; // 注入用户实体的数据库接口 @Autowired UserRepository userRepository; // 自动注入认证管理器 @Autowired AuthenticationManager authenticationManager; // 自动注入密码编码器 @Autowired PasswordEncoder encoder; // 自动注入JWT工具类 @Autowired JwtUtils jwtUtils; /** * 用户注册。 * @param reg 注册请求对象,包含用户名和密码 * @return 注册后的用户信息 */ public User register(RegisterRequest reg) { // 检查用户名是否已存在,如果存在则抛出异常 userRepository.findByUsername(reg.getUsername()).ifPresent(user -> { throw new ResourceExistedException("User","username="+reg.getUsername()); }); // 创建用户对象并设置信息 User user = new User(); // 设置密码,暂时使用明文密码 user.setPassword(encoder.encode(reg.getPassword())); // 设置昵称为用户名 user.setUsername(reg.getUsername()); // 设置昵称为用户名 user.setNickname(reg.getUsername()); // 设置默认头像 user.setAvatar(DEFAULT_HEAD_ICON); // 在数据库中创建用户 return userRepository.save(user); } /** * 用户登录,返回JWT令牌。 * @param username 用户名 * @param password 密码 * @return 包含JWT令牌的Map */ public Map login(String username, String password) { // 认证用户 Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password)); // 设置认证信息到SecurityContextHolder SecurityContextHolder.getContext() .setAuthentication(authentication); // 生成JWT令牌 String jwt = jwtUtils.generateJwtToken(authentication); // 返回包含JWT令牌的Map Map token = new HashMap<>(); token.put("token",jwt); return token; } } ``` #### 11. 通过swagger ui 测试登录接口 http://localhost:8080/api-docs ### 技术/工具需求: - [列出完成任务所需的技术栈、工具、软件版本等。] ### 成功标准: - [明确完成任务的评判标准,如代码功能实现、性能指标、测试通过条件等。] ### 扩展学习(可选): - [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。] ### 评估与反馈: - [说明如何提交作业、代码审查的标准、或任何反馈收集机制。] ### 时间估算: - [给出预计完成该任务所需的时间,帮助学生合理安排学习计划。]