SpringSecuity的使用

430 阅读5分钟

今天在使用SpringSecuity6.0时发现.authorizeRequests()已经被弃用了,发现网上的教程都是旧的,就写一篇关于最新稳定版的初步使用。 通过本教程学习可以通过密码使用jwt进行登录。

<!-- JWT API -->  
<dependency>  
<groupId>io.jsonwebtoken</groupId>  
<artifactId>jjwt-api</artifactId>  
<version>0.11.2</version>  
</dependency>  
<!-- JWT Implementation -->  
<dependency>  
<groupId>io.jsonwebtoken</groupId>  
<artifactId>jjwt-impl</artifactId>  
<version>0.11.2</version>  
</dependency>  
<!-- JWT and Jackson Integration -->  
<dependency>  
<groupId>io.jsonwebtoken</groupId>  
<artifactId>jjwt-jackson</artifactId>  
<version>0.11.2</version>  
</dependency>

前置知识

使用框架的密码登录的流程

  1. 首先前端会传递一个JOSN,包含用户名和密码,我们用DTO层接收,我们先思考一下,对于用户名和密码,我们得先从数据库查找到用户名然后和密码进行匹配,对于密码肯定不能明文存储,所以框架给我们提供了接口和密码存储的工具。
  2. 因为框架有很多验证方式,本文采用的就是账户密码的方式,所以使用UsernamePasswordAuthenticationToken来进行处理用户名和密码,对框架来说就是一个Authentication,它就是会保存你登录后一些信息,登录成功后需要设置在SecurityContext这样框架就可以的对用户进行认证授权。
  3. 同时我们还需要使用框架的接口来存储信息,一个是UserDetails,一个是UserDetailsService,UserDetails就是通过UserDetailsService的一个方法loadUserByUsername来从数据库获取信息,获取到的信息会被存在Authentication的principal属性上
  4. 这时就有个问题,你说来实现UsernamePasswordAuthenticationToken处理登录,框架怎么知道我写的是哪个服务的,所以这时就需要AuthenticationManager,它就是专门负责我们定义的处理方式,我们通过 DaoAuthenticationProvider来添加我们自定义的UserDetailsService UserDetails就可以了,DaoAuthenticationProvider就是AuthenticationManager的子类
  5. 最后因为密码要安全的存储到数据库,就需要添加框架的密码PasswordEncoder来添加自定义的存储方式

代码实现

  1. 配置securityFilterChain
//  SecurityConfig

@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class SecurityConfig {  
  
@NonNull JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;  
  
@NonNull CorsFilter corsFilter;  
  
@NonNull UserServiceImpl userService;  
  
  
@Bean  
public PasswordEncoder passwordEncoder() {  
return new BCryptPasswordEncoder();  
}  
  
  
@Bean  
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {  
return authenticationConfiguration.getAuthenticationManager();  
}  
  
@Bean  
public AuthenticationProvider authenticationProvider() {  
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();  
daoAuthenticationProvider.setUserDetailsService(userService);  
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());  
return daoAuthenticationProvider;  
}  
  
@Bean  
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
http  
.authorizeHttpRequests((authorizeHttpRequests) ->  
authorizeHttpRequests  
.requestMatchers("/", "/login", "/swagger-ui.html", "/register").permitAll()  
.requestMatchers("/swagger-resources/**").permitAll()  
.requestMatchers("/webjars/**").permitAll()  
.requestMatchers("/*/api-docs/**").anonymous()  
.requestMatchers(HttpMethod.GET, "/", "/*.html", "/*/*.html", "/*/*.css", "/*/*.js",  
"/profile/**").permitAll()  
.anyRequest().authenticated()  
)  
.csrf(AbstractHttpConfigurer::disable)  
.logout((logout) ->  
logout  
.logoutUrl("/logout")  
.logoutSuccessUrl("/logout/success")  
  
)  
.authenticationProvider(authenticationProvider())  
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)  
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);  
return http.build();  
}  
  
  
}
  1. 配置JWT
@Component  
@Slf4j  
@Data  
@RequiredArgsConstructor  
public class TokenProvider {  
/**  
* 自定义令牌标识  
*/  
@Value("${token.header}")  
private String header;  
  
/**  
* 令牌秘钥  
*/  
@Value("${token.secret}")  
private String secret;  
  
/**  
* 自动刷新token的时间,当过期时间不足autoRefreshTime的值的时候,会触发刷新用户登录缓存的时间  
* 比如这个值是20, 用户是8点登录的, 8点半缓存会过期, 当过8.10分的时候,就少于20分钟了,便触发  
* 刷新登录用户的缓存时间  
*/  
@Value("${token.autoRefreshTime}")  
private int autoRefreshTime;  
  
public String generateToken(Authentication authentication) {  
User userPrincipal = (User) authentication.getPrincipal();  
  
String authorities = authentication.getAuthorities().stream()  
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));  
  
return Jwts.builder()  
.setClaims(Map.of("aut", authorities))  
.setSubject(userPrincipal.getUsername())  
.setExpiration(DateUtil.offsetHour(DateUtil.date(), autoRefreshTime))  
.signWith(getKey())  
.compact();  
}  
  
  
public Claims parseToken(String token) {  
return Jwts.parserBuilder()  
.setSigningKey(getKey())  
.build()  
.parseClaimsJws(token).getBody();  
  
}  
  
  
public boolean validateToken(String authToken) {  
try {  
Claims claims = parseToken(authToken);  
return !isTokenExpired(claims);  
} catch (MalformedJwtException | UnsupportedJwtException |  
IllegalArgumentException jwtException) {  
log.error("Token validation error: {}", jwtException.getMessage());  
throw new ApiException(jwtException, ErrorCodeMsg.FAIL);  
}  
}  
  
private boolean isTokenExpired(Claims claims) {  
return claims.getExpiration().toInstant().isBefore(Instant.now());  
}  
  
/**  
* 生成key  
*  
* @return  
*/  
public Key getKey() {  
return new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());  
}  
}
  1. 配置JWTFilter
@Component  
@Slf4j  
@RequiredArgsConstructor  
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {  
  
@NonNull  
TokenProvider tokenProvider;  
  
@NonNull UserService userService;  
  
@Override  
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {  
Optional<String> jwt = resolveHeaderToken(request);  
  
if (jwt.isPresent() && tokenProvider.validateToken(jwt.get())) {  
String subject = tokenProvider.parseToken(jwt.get()).getSubject();  
UserDetails userDetails = userService.loadUserByUsername(subject);  
var authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());  
SecurityContextHolder.getContext().setAuthentication(authToken);  
}  
filterChain.doFilter(request, response);  
}  
  
  
private Optional<String> resolveHeaderToken(HttpServletRequest request) {  
String bearerToken = request.getHeader("Authorization");  
return StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith("Bearer ") ? Optional.of(bearerToken.substring(7)) : Optional.empty();  
}  
}
  1. 配置跨域
@Configuration  
public class FilterConfig {  
  
/**  
* 跨域配置  
*/  
@Bean  
public CorsFilter corsFilter() {  
CorsConfiguration config = new CorsConfiguration();  
config.setAllowCredentials(true);  
// 设置访问源地址  
config.addAllowedOriginPattern("*");  
// 设置访问源请求头  
config.addAllowedHeader("*");  
// 设置访问源请求方法  
config.addAllowedMethod("*");  
// 有效期 1800秒  
config.setMaxAge(1800L);  
// 添加映射路径,拦截一切请求  
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
source.registerCorsConfiguration("/**", config);  
// 返回新的CorsFilter  
return new CorsFilter(source);  
}  
  
  
}
  1. 配置Controller
@RestController  
@Slf4j  
@RequiredArgsConstructor  
@Tag(name = "登录API", description = "登录相关接口")  
public class LoginController {  
  
@NonNull  
private PasswordEncoder passwordEncoder;  
@NonNull  
private AuthenticationManager authenticationManager;  
@NonNull  
private TokenProvider tokenProvider;  
  
@NonNull  
private UserService userService;  
  
@GetMapping("/")  
public String home() {  
log.info("1");  
return "Hello Nexta1";  
}  
  
@PostMapping("/register")  
@Operation(summary = "注册")  
public ResponseDTO<Void> register(@Valid @RequestBody SignupDTO userDTO) {  
User user = new User();  
BeanUtils.copyProperties(userDTO, user);  
var encodePassword = passwordEncoder.encode(user.getPassword());  
user.setPassword(encodePassword);  
userService.save(user);  
return ResponseDTO.ok();  
}  
  
@PostMapping("/login")  
@Operation(summary = "登录")  
public ResponseDTO<String> login(@RequestBody @Valid LoginDTO loginDTO) {  
  
SecurityContext context = SecurityContextHolder.createEmptyContext();  
  
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(  
loginDTO.username(), loginDTO.password()));  
  
context.setAuthentication(authentication);  
  
String token = tokenProvider.generateToken(authentication);  
  
return ResponseDTO.ok(token);  
}  
}
  1. 配置DTO
@Data  
@AllArgsConstructor  
public class ResponseDTO<T> {  
  
private Integer code;  
  
private String msg;  
  
private T data;  
  
public static <T> ResponseDTO<T> ok() {  
return build(null, ErrorCodeMsg.SUCCESS);  
}  
  
public static <T> ResponseDTO<T> ok(T data) {  
return build(data, ErrorCodeMsg.SUCCESS);  
}  
  
public static <T> ResponseDTO<T> fail() {  
return build(null, ErrorCodeMsg.FAIL);  
}  
  
public static <T> ResponseDTO<T> fail(T data) {  
return build(data, ErrorCodeMsg.FAIL);  
}  
  
public static <T> ResponseDTO<T> fail(ErrorInterface code) {  
return build(null, code);  
}  
  
public static <T> ResponseDTO<T> fail(ErrorInterface code, Object... args) {  
return build(null, code, args);  
}  
  
public static <T> ResponseDTO<T> fail(ApiException exception) {  
return build(exception.getError().getCode(), exception.getError().getMsg());  
}  
  
  
public static <T> ResponseDTO<T> build(T data, ErrorInterface code, Object... args) {  
return new ResponseDTO<>(code.getCode(), StrUtil.format(code.getMsg(), args), data);  
}  
  
public static <T> ResponseDTO<T> build(Integer code, String msg) {  
return new ResponseDTO<>(code, msg, null);  
}  
  
}
// LoginDTO
public record LoginDTO(String username, @NotNull String password) {  
}
// SignUpDTO
@EqualsAndHashCode(callSuper = true)  
@Data  
public class SignupDTO extends UserDTO {  
String password;  
}
// UserDTO
@Data  
public class UserDTO {  
String username;  
}
  1. 配置MVC
@Entity  
@Getter  
@Setter  
@ToString  
@RequiredArgsConstructor  
@NoArgsConstructor  
@AllArgsConstructor  
@JsonIgnoreProperties(ignoreUnknown = true)  
public class User implements UserDetails {  
  
@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY)  
private Long id;  
private String username;  
private String password;  
  
@Override  
public Collection<? extends GrantedAuthority> getAuthorities() {  
return null;  
}  
  
@Override  
public boolean isAccountNonExpired() {  
return true;  
}  
  
@Override  
public boolean isAccountNonLocked() {  
return true;  
}  
  
@Override  
public boolean isCredentialsNonExpired() {  
return true;  
}  
  
@Override  
public boolean isEnabled() {  
return true;  
}  
  
@Override  
public boolean equals(Object o) {  
if (this == o) return true;  
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;  
User user = (User) o;  
return getId() != null && Objects.equals(getId(), user.getId());  
}  
  
@Override  
public int hashCode() {  
return getClass().hashCode();  
}  
}

public interface UserRepository extends JpaRepository<User, Long> {  
User findUserByUsername(String username);  
}
// interface
public interface UserService extends UserDetailsService, UserDetailsPasswordService {  
  
List<User> getUsers();  
  
User save(User user);  
}
// impl
@Service  
@RequiredArgsConstructor  
public class UserServiceImpl implements UserService {  
  
@NonNull  
private UserRepository userRepository;  
  
@Override  
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
User user = userRepository.findUserByUsername(username);  
if (user == null) {  
throw new ApiException(ErrorCodeMsg.FAIL);  
}  
return user;  
  
}  
  
@Override  
public List<User> getUsers() {  
return userRepository.findAll();  
}  
  
@Override  
public User save(User user) {  
return userRepository.save(user);  
}  
  
@Override  
public UserDetails updatePassword(UserDetails user, String newPassword) {  
return null;  
}  
}
  1. 配置错误码
public interface ErrorInterface {  
int getCode();  
  
String getMsg();  
}

public enum ErrorCodeMsg implements ErrorInterface {  
SUCCESS(0, "操作成功"),  
FAIL(-1, "操作失败");  
  
private final int code;  
private final String msg;  
  
ErrorCodeMsg(int code, String msg) {  
this.code = code;  
this.msg = msg;  
}  
  
@Override  
public int getCode() {  
return code;  
}  
  
@Override  
public String getMsg() {  
return msg;  
}  
}
@EqualsAndHashCode(callSuper = true)  
@Data  
public class ApiException extends RuntimeException {  
@Serial  
private static final long serialVersionUID = 1L;  
  
ErrorInterface error;  
  
public ApiException(Throwable e, ErrorInterface error) {  
}  
  
public ApiException(ErrorInterface error) {  
}  
}
  1. 配置拦截器
@RestControllerAdvice  
@Slf4j  
public class GlobalExceptionInterceptor {  
/**  
* 业务异常  
*/  
@ExceptionHandler(ApiException.class)  
public ResponseDTO<?> handleServiceException(ApiException e) {  
log.error(e.getMessage(), e);  
return ResponseDTO.fail(e);  
}  
}
  1. 配置yml
token:  
  header: Authorization  
  secret: V1ckAluCnzBH2vDfNH1ZZVyi/YiuVCTX+SKUUv30e3c=  
  autoRefreshTime: 12