目录
环境介绍
Spring Boot 3.1.8 + Spring Cloud 2022.0.5 + Spring Security 6.1.6
Spring Security
1. Spring Security 原理
1.1 概念
认证:就是可不可以登录系统
授权:有没有权限访问资源
权限模型:RBAC
1.2 流程图
2.Spring Security 搭建
2.1 项目目录介绍
main 父级工程
----parent 版本控制
----core 核心框架 依赖jar包
----common 公共依赖
----gateway 网关
----plateform 客户业务模块
----admin 后台模块
在本文中有两套登录系统一套为客户登录(plateform),一套为运营登录(admin)
两套登录复用一个core中的登录,并采用用户名密码和手机验证码两种登录方式
2.2 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>compile</scope>
</dependency>
2.3 自定义 UserDetail
@Data
public class User implements UserDetails {
private Long userId;
private String username;
private String password;
private Long organizationId;
private Collection<? extends GrantedAuthority> authorities = List.of();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}`
}
public interface UserService extends UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
2.4 配置Spring Security 忽略Path
可以使用使用配置类配置,在本文中将config相关配置放在了登录接口中实线
@Component
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//处理登录认证
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录过程处理
http.formLogin() //表单登录
.loginProcessingUrl("/api/user/login") //登录请求url地址
.successHandler(loginSuccessHandler) //认证成功
.failureHandler(loginFailureHandler) //认证失败
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建Session
.and().authorizeRequests() //设置需要拦截的请求
.antMatchers("/api/user/login").permitAll()//登录放行
.anyRequest().authenticated() //其他请求一律拦截
.and()
.exceptionHandling()
.authenticationEntryPoint(anonymousAuthenticationHandler) //匿名无权限类
.accessDeniedHandler(customerAccessDeniedHandler) //认证用户无权限
.and()
.cors();//支持跨域
}
本文配置白名单
@Configuration(proxyBeanMethods = false)
public class WebSecurityCustomizerConfig {
@Autowired
private IgnorePatternsProperty ignorePatternsProperty;
@Bean
public Collection<RequestMatcher> requestIgnoreRequestMatchers() {
Set<String> patterns = new HashSet<>();
if (ignorePatternsProperty != null &&
ignorePatternsProperty.getPatterns() != null) {
patterns.addAll(ignorePatternsProperty.getPatterns());
}
List<RequestMatcher> requestMatchers = new ArrayList<>();
for (String str : patterns) {
requestMatchers.add(AntPathRequestMatcher.antMatcher(str));
}
return requestMatchers;
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
Collection<RequestMatcher> requestMatchers = requestIgnoreRequestMatchers();
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
if (requestMatchers.isEmpty()) {
return;
}
web.ignoring()
.requestMatchers(requestMatchers.toArray(new RequestMatcher[0]));
}
};
}
@Bean
public WebMvcProperties webMvcProperties() {
WebMvcProperties properties = new WebMvcProperties();
// 设置其他属性,例如视图解析器、静态资源位置等
properties.setThrowExceptionIfNoHandlerFound(true);
return properties;
}
2.5 编写登录接口
public abstract class AbstractAuthServerController {
private final AuthenticationManager authenticationManager;
public AbstractAuthServerController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected ClaimUser authenticate(Authentication authenticationToken) {
Authentication authentication = authenticationManager.authenticate(authenticationToken);
User authenticatedUser = (User) authentication.getPrincipal();
ClaimUser claimUser = new ClaimUser();
claimUser.setId(authenticatedUser.getUserId());
claimUser.setUsername(authenticatedUser.getUsername());
claimUser.setOrganizationId(authenticatedUser.getOrganizationId());
claimUser.setAuthorities(AuthorityUtils.authorityListToSet(authenticatedUser.getAuthorities()));
return claimUser;
}
}
@Slf4j
@Tag(name = "DefaultAuthServerController")
@RequestMapping("/auth")
@RestController
@Validated
@Observed(name = "DefaultAuthServerController")
public class DefaultAuthServerController extends AbstractAuthServerController {
private final JwtService jwtService;
private final UserService userService;
private final ModelMapper modelMapper;
public DefaultAuthServerController(
JwtService jwtService,
AuthenticationManager authenticationManager,
UserService userService, ModelMapper modelMapper) {
super(authenticationManager);
this.jwtService = jwtService;
this.userService = userService;
this.modelMapper = modelMapper;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody @Validated LoginUsernameRequest request) {
userService.preUsernameLoginCheck(request);
try {
ClaimUser claimUser = super.authenticate(
new UsernameAuthenticationToken(request.getUsername(), request.getPassword(), request.getExtraInfo())
);
Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);
LoginResponse response = new LoginResponse();
response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));
userService.usernameLoginSuccessHandle(request, claimUser);
return ResponseEntity.ok(response);
} catch (Exception ex) {
log.error("Username login failed. message = {}", ex.getMessage());
userService.usernameLoginFailedHandle(request, ex);
throw ex;
}
}
@PostMapping(value = "/refresh", consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<LoginResponse> refresh(@RequestBody @NotBlank(message = "{act.refresh.token.not.blank}") String token) {
var claim = jwtService.extractAllClaims(token);
if (!jwtService.isTokenValid(token, claim.getSubject())) {
throw new ActBadRequestException(MessageSourceContext.getAccessor().getMessage("act.refresh.token.invalid"));
}
User user = userService.findById(Long.parseLong(claim.getSubject()));
ClaimUser claimUser = modelMapper.map(user, ClaimUser.class);
claimUser.setId(user.getUserId());
Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);
LoginResponse response = new LoginResponse();
response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));
return ResponseEntity.ok(response);
}
@Slf4j
@Tag(name = "SmsAuthServerController")
@RequestMapping("/auth/sms")
@RestController
@Observed(name = "SmsAuthServerController")
public class SmsAuthServerController extends AbstractAuthServerController {
private final JwtService jwtService;
private final UserService userService;
public SmsAuthServerController(JwtService jwtService,
AuthenticationManager authenticationManager,
UserService userService) {
super(authenticationManager);
this.jwtService = jwtService;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> authenticate(@RequestBody @Validated LoginSmsVerifyCodeRequest request) {
userService.preSMSLoginCheck(request);
try {
ClaimUser claimUser = super.authenticate(
new SmsAuthenticationToken(request.getPhoneNumber(), request.getVerifyCode(), request.getExtraInfo())
);
Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);
LoginResponse response = new LoginResponse();
response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));
userService.smsLoginSuccessHandle(request, claimUser);
return ResponseEntity.ok(response);
} catch (Exception ex) {
log.error("SMS login failed. message = {}", ex.getMessage());
userService.smsLoginFailedHandle(request, ex);
throw ex;
}
}
2.6 进入认证流程
username认证流程
- 将请求信息封装到Authentication实现UserNameAuthenticationToken
调用AuthenticationManage下的ProviderManager.authenticate(UserNameAuthenticationToken) - 调用provider.authenticate委托认证AuthenticationProvider
- 实现AuthenticationProvider,重写authenticate()方法
- 调用 retrieveUser(),在方法里查询user信息返回UserDetails
2.6.1 封装认证信息
public class UsernameAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
private final Object extraInfo;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernameAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*/
public UsernameAuthenticationToken(Object principal, Object credentials, Object extraInfo) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.extraInfo = extraInfo;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public UsernameAuthenticationToken(Object principal, Object credentials, Object extraInfo,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.extraInfo = extraInfo;
super.setAuthenticated(true); // must use super, as we override
}
/**
* This factory method can be safely used by any code that wishes to create a
* unauthenticated <code>UsernameAuthenticationToken</code>.
*
* @param principal
* @param credentials
* @return UsernameAuthenticationToken with false isAuthenticated() result
* @since 5.7
*/
public static UsernameAuthenticationToken unauthenticated(Object principal, Object credentials, Object extraInfo) {
return new UsernameAuthenticationToken(principal, credentials, extraInfo);
}
/**
* This factory method can be safely used by any code that wishes to create a
* authenticated <code>UsernameAuthenticationToken</code>.
*
* @param principal
* @param credentials
* @param extraInfo
* @return UsernameAuthenticationToken with true isAuthenticated() result
* @since 5.7
*/
public static UsernameAuthenticationToken authenticated(Object principal, Object credentials, Object extraInfo,
Collection<? extends GrantedAuthority> authorities) {
return new UsernameAuthenticationToken(principal, credentials, extraInfo, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public String getExtraInfo() {
return extraInfo == null ? null : extraInfo.toString();
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
@Slf4j
@Tag(name = "SmsAuthServerController")
@RequestMapping("/auth/sms")
@RestController
@Observed(name = "SmsAuthServerController")
public class SmsAuthServerController extends AbstractAuthServerController {
private final JwtService jwtService;
private final UserService userService;
public SmsAuthServerController(JwtService jwtService,
AuthenticationManager authenticationManager,
UserService userService) {
super(authenticationManager);
this.jwtService = jwtService;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> authenticate(@RequestBody @Validated LoginSmsVerifyCodeRequest request) {
userService.preSMSLoginCheck(request);
try {
ClaimUser claimUser = super.authenticate(
new SmsAuthenticationToken(request.getPhoneNumber(), request.getVerifyCode(), request.getExtraInfo())
);
Map<String, String> jwtTokenPair = jwtService.generateToken(claimUser);
LoginResponse response = new LoginResponse();
response.setTokenType(Constant.DEFAULT_TOKEN_TYPE);
response.setAccessToken(jwtTokenPair.get(Constant.ACCESS_TOKEN));
response.setAccessExpiresIn(jwtService.getAccessExpirationTime());
response.setRefreshToken(jwtTokenPair.get(Constant.REFRESH_TOKEN));
userService.smsLoginSuccessHandle(request, claimUser);
return ResponseEntity.ok(response);
} catch (Exception ex) {
log.error("SMS login failed. message = {}", ex.getMessage());
userService.smsLoginFailedHandle(request, ex);
throw ex;
}
}
}
2.6.2 自定义实现AuthenticationProvider
public class UsernameAuthenticationProvider extends AbstractUsernameAuthenticationProvider {
/**
* The plaintext password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not found
* to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
/**
* The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
* on when the user is not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public UsernameAuthenticationProvider() {
this(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* Creates a new instance using the provided {@link PasswordEncoder}
*
* @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null.
* @since 6.0.3
*/
public UsernameAuthenticationProvider(PasswordEncoder passwordEncoder) {
setPasswordEncoder(passwordEncoder);
}
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernameAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("act.login.bad.credentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (StringUtils.isEmpty(userDetails.getPassword()) || !this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new ActBadPasswordException(this.messages
.getMessage("act.login.bad.password", "Bad Password"));
}
}
@Override
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
@Override
protected final UserDetails retrieveUser(String username, UsernameAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
if (!(this.getUserDetailsService() instanceof UserService userService)) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
UserDetails loadedUser = userService.findByUsername(username, authentication.getExtraInfo());
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal, UsernameAuthenticationToken authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private void mitigateAgainstTimingAttack(UsernameAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
* not set, the password will be compared using
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
*
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
* types.
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
public class SmsAuthenticationProvider extends AbstractSmsAuthenticationProvider {
private UserDetailsService userDetailsService;
private SmsVerifyCodeService smsVerifyCodeService;
public SmsAuthenticationProvider() {
}
@Override
protected void additionalAuthenticationChecks(String phoneNumber,
SmsAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractSmsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String inputVerifyCode = authentication.getCredentials().toString();
String verifyCode = smsVerifyCodeService.loadVerifyCodeByPhoneNumber(phoneNumber);
if (!verifyCode.matches(inputVerifyCode)) {
this.logger.debug("Failed to authenticate since verify code does not match stored value");
throw new ActBadSmsVerifyCodeException(this.messages
.getMessage("act.login.bad.sms.verify.code", "Bad sms verify code"));
}
}
@Override
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
@Override
protected final UserDetails retrieveUser(String phoneNumber, SmsAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
if (!(userDetailsService instanceof UserService userService)) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
UserDetails loadedUser = userService.findByPhoneNumber(phoneNumber, authentication.getExtraInfo());
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (ActPhoneNumberNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal, SmsAuthenticationToken authentication,
UserDetails user) {
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
}
private void mitigateAgainstTimingAttack(SmsAuthenticationToken authentication) {
}
public void setSmsVerifyCodeService(SmsVerifyCodeService smsVerifyCodeService) {
Assert.notNull(smsVerifyCodeService, "smsVerifyCodeService cannot be null");
this.smsVerifyCodeService = smsVerifyCodeService;
}
protected SmsVerifyCodeService getSmsVerifyCodeService() {
return this.smsVerifyCodeService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
}
2.7 生成JWT token
@Service
public class JwtServiceImpl implements JwtService {
@Autowired
private JwtSecurityProperty jwtSecurityProperty;
@Override
public Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
@Override
public String extractSubject(String token) {
return extractClaim(token, Claims::getSubject);
}
@Override
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
@Override
public Map<String, String> generateToken(ClaimUser claimUser) {
Map<String, Object> accessClaims = new HashMap<>();
accessClaims.put("id", claimUser.getId());
accessClaims.put("username", claimUser.getUsername());
accessClaims.put("organizationId", claimUser.getOrganizationId());
accessClaims.put("authorities", claimUser.getAuthorities());
Map<String, Object> refreshClaims = new HashMap<>();
refreshClaims.put("id", claimUser.getId());
return generateTokenPair(accessClaims, refreshClaims,
claimUser.getUsername(),
String.valueOf(claimUser.getId()),
System.currentTimeMillis(),
jwtSecurityProperty.getAccessExpirationTime(),
jwtSecurityProperty.getRefreshExpirationTime());
}
public Map<String, String> generateTokenPair(Map<String, Object> accessClaims,
Map<String, Object> refreshClaims,
String accessTokenSubject,
String refreshTokenSubject,
long issuedAt,
long accessTokenExpiration,
long refreshTokenExpiration) {
Map<String, String> tokenPair = new HashMap<>();
String accessToken = buildToken(accessClaims, accessTokenSubject, issuedAt, accessTokenExpiration);
tokenPair.put(Constant.ACCESS_TOKEN, accessToken);
String refreshToken = buildToken(refreshClaims, refreshTokenSubject, issuedAt, refreshTokenExpiration);
tokenPair.put(Constant.REFRESH_TOKEN, refreshToken);
return tokenPair;
}
public long getAccessExpirationTime() {
return jwtSecurityProperty.getAccessExpirationTime();
}
public long getRefreshExpirationTime() {
return jwtSecurityProperty.getRefreshExpirationTime();
}
private String buildToken(
Map<String, Object> extraClaims,
String subject,
long issuedAt,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(subject)
.setIssuedAt(new Date(issuedAt + expiration))
.setExpiration(new Date(issuedAt + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, String subject) {
return (extractSubject(token).equals(subject)) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return
extractClaim(token, Claims::getExpiration);
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperty.getSecretKey());
return Keys.hmacShaKeyFor(keyBytes);
}
}
2.8 访问服务解析JWT token
2.8.1 JWT token 解析过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Collection<RequestMatcher> requestIgnoreRequestMatchers;
private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtService jwtService;
public JwtAuthenticationFilter(
Collection<RequestMatcher> requestIgnoreRequestMatchers, JwtService jwtService,
HandlerExceptionResolver handlerExceptionResolver
) {
this.requestIgnoreRequestMatchers = requestIgnoreRequestMatchers;
this.jwtService = jwtService;
this.handlerExceptionResolver = handlerExceptionResolver;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ") || isIgnoreRequests(request)) {
filterChain.doFilter(request, response);
return;
}
try {
final String jwt = authHeader.substring(7);
final var claims = jwtService.extractAllClaims(jwt);
final String username = claims.getSubject();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (username != null && authentication == null) {
if (jwtService.isTokenValid(jwt, username)) {
ClaimUser claimUser = convert(claims);
Collection<? extends GrantedAuthority> authorities
= AuthorityUtils.createAuthorityList(claimUser.getAuthorities().toArray(new String[0]));
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
claimUser,
null,
authorities
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
} catch (Exception exception) {
handlerExceptionResolver.resolveException(request, response, null, exception);
}
}
private ClaimUser convert(Claims claims) {
ClaimUser claimUser = new ClaimUser();
claimUser.setId(claims.get("id", Long.class));
claimUser.setOrganizationId(claims.get("organizationId", Long.class));
claimUser.setUsername(claims.getSubject());
var data = claims.get("authorities", Collection.class);
List<String> authorities = new ArrayList<>();
for (Object obj : data) {
authorities.add(obj.toString());
}
claimUser.setAuthorities(authorities);
return claimUser;
}
private boolean isIgnoreRequests(HttpServletRequest request) {
for (RequestMatcher requestMatcher : this.requestIgnoreRequestMatchers) {
if (requestMatcher.matches(request)) {
return true;
}
}
return false;
}
}
2.8.2 配置JWT token过滤器
@Slf4j
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Configuration(proxyBeanMethods = false)
public class AuthServerSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public AuthServerSecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter
) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.exceptionHandling(exceptionHandlingConfig -> exceptionHandlingConfig
.authenticationEntryPoint(new UnauthorizedEntryPoint()))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
2.9 将core中的登录相关bean注入登录Service
2.9.1 创建注解,导入core config
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({AuthServerAppConfig.class})
public @interface EnableActsAuthServer {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({SmsAuthServerController.class})
public @interface EnableActsAuthSmsLogin {
}
2.9.2 创建 config
@Configuration
@Import({AuthResourceSecurityConfig.class,
WebSecurityCustomizerConfig.class,
PasswordEncoderConfig.class,
WebMvcConfig.class,
GrpcAuthConfig.class,
RedisCacheConfig.class,
ModelMapperConfig.class,
MessageSourceConfig.class,
OpenAPISecurityConfig.class,
ObservedAspectConfig.class,
GrpcZipkinConfig.class})
@ComponentScans(value = {
@ComponentScan(value = "com.core.properties"),
@ComponentScan(value = "com.core.filter"),
@ComponentScan(value = "com.core.component"),
@ComponentScan(value = "com.core.exceptions"),
@ComponentScan(value = "com.core.utils"),
@ComponentScan(value = "com.core.controller", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {
SmsAuthServerController.class
})
}),
@ComponentScan(value = "com.core.service")
})
public class AuthServerAppConfig {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@Autowired(required = false)
private SmsVerifyCodeService smsVerifyCodeService;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthServerAppConfig(UserService userService, PasswordEncoder passwordEncoder, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
authenticationManagerBuilder.authenticationProvider(authenticationProvider());
AuthenticationProvider smsAuthenticationProvider = smsAuthenticationProvider();
if (smsAuthenticationProvider != null) {
authenticationManagerBuilder.authenticationProvider(smsAuthenticationProvider());
}
return config.getAuthenticationManager();
}
@Bean
AuthenticationProvider authenticationProvider() {
UsernameAuthenticationProvider authProvider = new UsernameAuthenticationProvider();
authProvider.setUserDetailsService(userService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
private AuthenticationProvider smsAuthenticationProvider() {
if (smsVerifyCodeService == null) {
return null;
}
SmsAuthenticationProvider authProvider = new SmsAuthenticationProvider();
authProvider.setUserDetailsService(userService);
authProvider.setSmsVerifyCodeService(smsVerifyCodeService);
return authProvider;
}
}
2.9.3 启动类添加注解
@EnableActsAuthSmsLogin
public class PlatformApplication {
public static void main(String[] args) {
SpringApplication.run(PlatformApplication.class, args);
}
}
@EnableActsAuthServer
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
致谢
感谢领导的指导