[add]jwt
This commit is contained in:
parent
086b8b85d3
commit
20ae17afeb
@ -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'
|
||||
|
@ -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<WhiteListRule> whiteList = new ArrayList<>();
|
||||
private List<String> allowedOrigins;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class WhiteListRule {
|
||||
private String path;
|
||||
private List<String> 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<? extends GrantedAuthority> 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。
|
||||
* <p>
|
||||
* 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<String, Object> 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<String, HttpMethod> 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<Map<String,String>> 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<String,String> 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<String,String> token = new HashMap<>();
|
||||
token.put("token",jwt);
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 11. 通过swagger ui 测试登录接口
|
||||
|
||||
http://localhost:8080/api-docs
|
||||
|
||||
|
||||
|
||||
### 技术/工具需求:
|
||||
- [列出完成任务所需的技术栈、工具、软件版本等。]
|
||||
|
26
src/main/java/com/lk/paopao/conf/SecurityProperties.java
Normal file
26
src/main/java/com/lk/paopao/conf/SecurityProperties.java
Normal file
@ -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<WhiteListRule> whiteList = new ArrayList<>();
|
||||
private List<String> allowedOrigins;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class WhiteListRule {
|
||||
private String path;
|
||||
private List<String> methods;
|
||||
}
|
||||
}
|
@ -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<Map<String,String>> login(@RequestBody LoginRequest loginRequest){
|
||||
return ResultUtil.ok(authService.login(loginRequest.getUsername(),loginRequest.getPassword()));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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停用")
|
||||
|
57
src/main/java/com/lk/paopao/security/AuthEntryPointJwt.java
Normal file
57
src/main/java/com/lk/paopao/security/AuthEntryPointJwt.java
Normal file
@ -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。
|
||||
* <p>
|
||||
* 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<String, Object> 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);
|
||||
}
|
||||
}
|
93
src/main/java/com/lk/paopao/security/AuthTokenFilter.java
Normal file
93
src/main/java/com/lk/paopao/security/AuthTokenFilter.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
99
src/main/java/com/lk/paopao/security/JwtUtils.java
Normal file
99
src/main/java/com/lk/paopao/security/JwtUtils.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
149
src/main/java/com/lk/paopao/security/SecurityConfiguration.java
Normal file
149
src/main/java/com/lk/paopao/security/SecurityConfiguration.java
Normal file
@ -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<String, HttpMethod> 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);
|
||||
}
|
||||
}
|
70
src/main/java/com/lk/paopao/security/UserDetailsImpl.java
Normal file
70
src/main/java/com/lk/paopao/security/UserDetailsImpl.java
Normal file
@ -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<? extends GrantedAuthority> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<String,String> 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<String,String> token = new HashMap<>();
|
||||
token.put("token",jwt);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -31,3 +31,18 @@ app:
|
||||
version: v1
|
||||
default:
|
||||
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"
|
Loading…
Reference in New Issue
Block a user