diff --git a/build.gradle b/build.gradle index 53dbf8e..85473cf 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' diff --git a/docs/tasks/任务7-Sprint-1-基于JWT的用户认证实现.md b/docs/tasks/任务7-Sprint-1-基于JWT的用户认证实现.md index 84a0f0c..06797c7 100644 --- a/docs/tasks/任务7-Sprint-1-基于JWT的用户认证实现.md +++ b/docs/tasks/任务7-Sprint-1-基于JWT的用户认证实现.md @@ -6,10 +6,738 @@ - [列出完成该任务前学生应具备的基础知识或先修技能,如特定编程语言基础、框架了解等。] ### 操作步骤: -1. **步骤1**: [详细描述第一步操作,包括使用的工具、命令或技术要点。] -2. **步骤2**: [继续描述后续步骤,确保每一步都清晰、可执行。] - ... - [根据需要添加更多步骤] +#### 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:8080" +``` +实现安全相关自定义配置的配置类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()).orElseThrow(() -> new ResourceExistedException("User","useranme="+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 + + ### 技术/工具需求: - [列出完成任务所需的技术栈、工具、软件版本等。] diff --git a/src/main/java/com/lk/paopao/conf/SecurityProperties.java b/src/main/java/com/lk/paopao/conf/SecurityProperties.java new file mode 100644 index 0000000..5be71d0 --- /dev/null +++ b/src/main/java/com/lk/paopao/conf/SecurityProperties.java @@ -0,0 +1,26 @@ +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; + } +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/controller/AuthController.java b/src/main/java/com/lk/paopao/controller/AuthController.java index cb78b9a..f938b96 100644 --- a/src/main/java/com/lk/paopao/controller/AuthController.java +++ b/src/main/java/com/lk/paopao/controller/AuthController.java @@ -1,5 +1,6 @@ package com.lk.paopao.controller; +import com.lk.paopao.dto.rest.request.LoginRequest; import com.lk.paopao.dto.rest.request.RegisterRequest; import com.lk.paopao.dto.rest.response.DataResult; import com.lk.paopao.dto.rest.response.ResultUtil; @@ -11,6 +12,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Map; + @RestController @RequestMapping("${app.version}/auth") public class AuthController { @@ -27,4 +30,10 @@ public class AuthController { User saved = authService.register(req); return ResultUtil.ok(saved); } + + @PostMapping("/login") + public DataResult> login(@RequestBody LoginRequest loginRequest){ + return ResultUtil.ok(authService.login(loginRequest.getUsername(),loginRequest.getPassword())); + } + } diff --git a/src/main/java/com/lk/paopao/dto/rest/request/LoginRequest.java b/src/main/java/com/lk/paopao/dto/rest/request/LoginRequest.java new file mode 100644 index 0000000..7f100e3 --- /dev/null +++ b/src/main/java/com/lk/paopao/dto/rest/request/LoginRequest.java @@ -0,0 +1,11 @@ +package com.lk.paopao.dto.rest.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + String username; + String password; +} diff --git a/src/main/java/com/lk/paopao/entity/User.java b/src/main/java/com/lk/paopao/entity/User.java index b2de521..5ff8812 100644 --- a/src/main/java/com/lk/paopao/entity/User.java +++ b/src/main/java/com/lk/paopao/entity/User.java @@ -37,7 +37,7 @@ public class User { private String phone; @Comment("密码") - @Column(name = "password", nullable = false, length = 32) + @Column(name = "password", nullable = false, length = 64) private String password; @Comment("状态,1正常,2停用") diff --git a/src/main/java/com/lk/paopao/security/AuthEntryPointJwt.java b/src/main/java/com/lk/paopao/security/AuthEntryPointJwt.java new file mode 100644 index 0000000..f5d4b48 --- /dev/null +++ b/src/main/java/com/lk/paopao/security/AuthEntryPointJwt.java @@ -0,0 +1,57 @@ +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); + } +} diff --git a/src/main/java/com/lk/paopao/security/AuthTokenFilter.java b/src/main/java/com/lk/paopao/security/AuthTokenFilter.java new file mode 100644 index 0000000..13ac4f8 --- /dev/null +++ b/src/main/java/com/lk/paopao/security/AuthTokenFilter.java @@ -0,0 +1,93 @@ +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; + } +} + + diff --git a/src/main/java/com/lk/paopao/security/JwtUtils.java b/src/main/java/com/lk/paopao/security/JwtUtils.java new file mode 100644 index 0000000..f9997f3 --- /dev/null +++ b/src/main/java/com/lk/paopao/security/JwtUtils.java @@ -0,0 +1,99 @@ +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; + } +} diff --git a/src/main/java/com/lk/paopao/security/SecurityConfiguration.java b/src/main/java/com/lk/paopao/security/SecurityConfiguration.java new file mode 100644 index 0000000..c37f726 --- /dev/null +++ b/src/main/java/com/lk/paopao/security/SecurityConfiguration.java @@ -0,0 +1,149 @@ +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); + } +} diff --git a/src/main/java/com/lk/paopao/security/UserDetailsImpl.java b/src/main/java/com/lk/paopao/security/UserDetailsImpl.java new file mode 100644 index 0000000..69a9e7b --- /dev/null +++ b/src/main/java/com/lk/paopao/security/UserDetailsImpl.java @@ -0,0 +1,70 @@ +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; + } + +} diff --git a/src/main/java/com/lk/paopao/security/UserDetailsServiceImpl.java b/src/main/java/com/lk/paopao/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..4a52ae4 --- /dev/null +++ b/src/main/java/com/lk/paopao/security/UserDetailsServiceImpl.java @@ -0,0 +1,39 @@ +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); + } +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/service/AuthService.java b/src/main/java/com/lk/paopao/service/AuthService.java index 5823d79..78cdf13 100644 --- a/src/main/java/com/lk/paopao/service/AuthService.java +++ b/src/main/java/com/lk/paopao/service/AuthService.java @@ -1,24 +1,41 @@ 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 { - // 注入用户实体的数据库接口 - @Autowired - UserRepository userRepository; // 从配置文件中读取默认头像地址 @Value("${app.default.head-icon}") private String DEFAULT_HEAD_ICON; + // 注入用户实体的数据库接口 + @Autowired + UserRepository userRepository; + // 自动注入认证管理器 + @Autowired + AuthenticationManager authenticationManager; + // 自动注入密码编码器 + @Autowired + PasswordEncoder encoder; + // 自动注入JWT工具类 + @Autowired + JwtUtils jwtUtils; /** * 用户注册。 @@ -27,11 +44,13 @@ public class AuthService { */ public User register(RegisterRequest reg) { // 检查用户名是否已存在,如果存在则抛出异常 - userRepository.findByUsername(reg.getUsername()).orElseThrow(() -> new ResourceExistedException("User","useranme="+reg.getUsername())); + userRepository.findByUsername(reg.getUsername()).ifPresent(user -> { + throw new ResourceExistedException("User","username="+reg.getUsername()); + }); // 创建用户对象并设置信息 User user = new User(); // 设置密码,暂时使用明文密码 - user.setPassword(reg.getPassword()); + user.setPassword(encoder.encode(reg.getPassword())); // 设置昵称为用户名 user.setUsername(reg.getUsername()); // 设置昵称为用户名 @@ -41,4 +60,28 @@ public class AuthService { // 在数据库中创建用户 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; + } + + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index afaeb53..667a55b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,4 +30,19 @@ spring: app: version: v1 default: - head-icon: https://assets.paopao.info/public/avatar/default/joshua.png \ No newline at end of file + head-icon: https://assets.paopao.info/public/avatar/default/joshua.png + 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:8080" \ No newline at end of file