在微服务中,整合Spring Security功能。将系统模块(涵盖用户、菜单等功能的模块)与Spring Security进行分离,使Spring Security作为一个单独的依赖存在,使用自动配置的方式配置进使用该依赖的模块当中。
处理流程
sequenceDiagram
participant f as 前端
participant s1 as 认证服务
participant s2 as 业务服务
f->>s1: 携带用户名密码访问登录接口
s1->>s1: 与数据库中的用户名密码对比
s1->>s1: 如果对比成功,生成JWT
Note right of s1: JWT携带用户Id。具体用户信息存放进Redis(userId:User),其中用户信息包含着用户的基础信息和权限信息
s1->>f: 响应JWT
f->>s2: 访问其他接口
s2->>s2: 获取请求头中的JWT
alt JWT != null
s2->>s2: 对JWT进行解析获取用户Id,然后从Redis中获取具体的用户信息
else JWT == null
s2->>f: 响应未登录
end
s2->>s2: 获取权限列表
alt 有权限
s2->>s2: 执行后续操作
s2->>f: 响应结果
else 无权限
s2->>f: 响应无权限
end
Spring Security 处理流程
Spring Security的原理是一个过滤器链,内部包含了提供各种功能的过滤器
sequenceDiagram
participant d as ……
participant a as UsernamePasswordAuthenticationFilter
participant d2 as ……
participant b as ExceptionTranslationFilter
participant c as FilterSecurityInterceptor
participant api as API
d->>a: ;
a->>d2: ;
d2->>b: ;
b->>c: ;
c->>api: ;
api->>c: ;
c->>b: ;
b->>d2: ;
d2->>a: ;
a->>d: ;
可以从Ioc中获取SecurityFilterChain对象,调用getFilters方法得到过滤器链(15个)
其中:
- UsernamePasswordAuthenticationFilter:负责处理在登陆页面的登陆请求
- ExceptionTranslationFilter:处理过滤器链中抛出的任何异常,然后转化成
AccessDeniedException或AuthenticationException - FilterSecurityInterceptor:负责权限校验的过滤器
认证流程
// TODO
整合 Spring Security
实现UserDetailsService接口
重写loadUserByUsername方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户信息
...
if (Objects.isNull(user)) {
throw new BadCredentialsException("用户不存在");
}
// 获取权限列表
...
return new LoginUser(user, new ArrayList<>(list));
}
}
实现UserDetails接口
将用户对象、用户权限作为成员变量。其中GrantedAuthority无法被序列化,所以需要使用额外的对象来存储权限信息:permissions,并且重写getAuthorities方法
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private UserEntity user;
private List<String> permissions;
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
public LoginUser(UserEntity user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getBcPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
...
}
完成上述操作,就已经可以在
Spring Security的原本配置中,使用/login接口进行登录验证了
自定义登录接口
登录成功之后将用户信息存入Redis中
@Service
public class LoginServiceImpl implements LoginService {
@Override
public R login(UserEntity user) {
// 创建Authentication对象,然后进行认证操作
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),
user.getBcPassword());
Authentication authenticate = SpringUtil.getBean(AuthenticationManager.class).authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
// 获取用户Id,作为Redis的Key,User作为Value存入Redis中
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
...
}
@Override
public R logout() {
// 从上下文中获取用户Id,然后从Redis中移除
...
}
}
定义JWT过滤器
使用继承OncePreRequestFilter的方式。作用是获取Token并且解析,然后进行用户是否登录的验证
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的JWT
...
// 解析JWT,获取用户Id,从Redis中获取用户信息和权限列表
...
// 完成认证,然后将认证信息存入上下文中,最后放行
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
Spring Security核心配置
将登录接口进行放行。重写WebSecurityConfigurerAdapter中的configure方法
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityAutoConfiguration extends WebSecurityConfigurerAdapter {
@Resource
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
public DefaultAuthenticationEntryPoint authenticationEntryPoint;
@Resource
public DefaultAccessDeniedHandler accessDeniedHandler;
@Resource
public SecurityProperties properties;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
if (!CollectionUtils.isEmpty(properties.getAnonymous())) {
for (String anonymous : properties.getAnonymous()) {
http.authorizeRequests().antMatchers(anonymous).anonymous();
}
}
http.authorizeRequests().anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证过程中出现的异常
源码流程
// TODO
自定义认证失败处理器
实现AuthenticationEntryPoint接口
@Component
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
HttpUtil.response(response, 50000 + HttpStatus.UNAUTHORIZED.value(), "认证失败");
}
}
自定义拒绝访问处理器
实现AccessDeniedHandler接口
@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
HttpUtil.response(response, 50000 + HttpStatus.FORBIDDEN.value(), accessDeniedException.getMessage());
}
}
跨域
除了在configure中配置Spring Security的跨域问题:cors,还需要打开Servlet的跨域:
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
或者使用注解:@CrossOrigin