Spring security一个重量级安全框架,可以用来做权限认证和访问控制。下面以一个实际项目来进行filter+token的验证。
表设计很简单,用户,权限和角色三张表加联系三张表的两个中间表。
│ WebSecurityConfigurer.java
│
├─detail
│ │ CustomUserDetailsService.java
│ │
│ └─entity
│ CustomUserDetailsUser.java
│
├─handler
│ CustomAuthenticationFailHandler.java
│ CustomAuthenticationSuccessHandler.java
│ CustomLogoutSuccessHandler.java
│ TokenAuthenticationFailHandler.java
│ └─provider CustomDaoAuthenticationProvider.java
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private PermissionsService permissionsService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));
if(ObjectUtil.isNull(sysUser)){
throw new UsernameNotFoundException("用户不存在");
}
return getDetail(sysUser);
}
public UserDetails loadUserByUserId(Long userId) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getById(userId);
if(ObjectUtil.isNull(sysUser)){
throw new UsernameNotFoundException("用户不存在");
}
return getDetail(sysUser);
}
private UserDetails getDetail(SysUser sysUser){
Set<String> permissions = permissionsService.getUserPermissions(sysUser.getUserId());
String[] roles = new String[0];
if(CollUtil.isNotEmpty(permissions)){
roles = permissions.stream().map(role -> "ROLE_" + role).toArray(String[]::new);
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles);
CustomUserDetailsUser customUserDetailsUser = new CustomUserDetailsUser(sysUser.getUserId(),sysUser.getUsername(),sysUser.getPassword(),authorities);
return customUserDetailsUser;
}
}
2.创建一个springsecurity的配置类,其中有一个过滤链,过滤链中要配置登录的请求地址(并不是登录页面地址)
.loginProcessingUrl(Constant.TOKEN_ENTRY_POINT_URL) \\路径配置在常量中
配置了这个路径后,springsecurity会去自动调用实现了UserDetailService接口的类中的loadUserByUsername方法去校验用户名密码,成功后将在springsecurity上下文环境中存入权限列表。
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private AuthIgnoreConfig authIgnoreConfig;
//过滤链,接收一条请求后的操作
@SneakyThrows
@Override
protected void configure(HttpSecurity http) {
List<String> permitAll = authIgnoreConfig.getIgnoreUrls();
permitAll.add("/actuator/**");
permitAll.add("/error");
permitAll.add("/v2/**");
permitAll.add(Constant.TOKEN_ENTRY_POINT_URL);
String[] urls = permitAll.stream().distinct().toArray(String[]::new);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
registry.antMatchers(urls).permitAll().anyRequest().authenticated().and().csrf().disable();
http
// 基于token,所以不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().disable()
.and()
.formLogin()
.loginProcessingUrl(Constant.TOKEN_ENTRY_POINT_URL)
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.and()
.logout()
.logoutUrl(Constant.TOKEN_LOGOUT_URL)
.addLogoutHandler(logoutHandler())
.logoutSuccessUrl("/sys/logout")
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(new TokenAuthenticationFailHandler())
.and()
// 如果不用验证码,注释这个过滤器即可
.addFilterBefore(new ValidateCodeFilter(redisTemplate,authenticationFailureHandler()),UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AuthenticationTokenFilter(authenticationManagerBean(),redisTemplate,customUserDetailsService), UsernamePasswordAuthenticationFilter.class);
}
// handler中权限验证失败的情况
@Bean
public AuthenticationFailureHandler authenticationFailureHandler(){
return new CustomAuthenticationFailHandler();
}
@Bean
public LogoutHandler logoutHandler(){
return new CustomLogoutSuccessHandler();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
.successHandler(authenticationSuccessHandler())
@Slf4j
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisTemplate redisTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
String token;
Long userId = 0l;
if(authentication.getPrincipal() instanceof CustomUserDetailsUser){
CustomUserDetailsUser userDetailsUser = (CustomUserDetailsUser) authentication.getPrincipal();
token = SecureUtil.md5(userDetailsUser.getUsername() + System.currentTimeMillis());
userId = userDetailsUser.getUserId();
}else {
token = SecureUtil.md5(String.valueOf(System.currentTimeMillis()));
}
redisTemplate.opsForValue().set(Constant.AUTHENTICATION_TOKEN + token,token,Constant.TOKEN_EXPIRE, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(token,userId,Constant.TOKEN_EXPIRE, TimeUnit.SECONDS);
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(R.ok().put(Constant.TOKEN,token)));
}
}
public class AuthenticationTokenFilter extends BasicAuthenticationFilter {
private RedisTemplate redisTemplate;
private CustomUserDetailsService customUserDetailsService;
private ObjectMapper objectMapper = new ObjectMapper();
public AuthenticationTokenFilter(AuthenticationManager authenticationManager,RedisTemplate template,CustomUserDetailsService customUserDetailsService) {
super(authenticationManager);
this.redisTemplate = template;
this.customUserDetailsService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(Constant.TOKEN);
if(StrUtil.isBlank(token) || StrUtil.equals(token,"null")){
token = request.getParameter(Constant.TOKEN);
}
if(StrUtil.isNotBlank(token) && !StrUtil.equals(token,"null")){
Object userId = redisTemplate.opsForValue().get(token);
if(ObjectUtil.isNull(userId)){
writer(response,"无效token");
return;
}
UserDetails userDetails = customUserDetailsService.loadUserByUserId((Long) userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
@SneakyThrows
public void writer(HttpServletResponse response,String msg){
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(objectMapper.writeValueAsString(R.error(HttpServletResponse.SC_UNAUTHORIZED,msg)));
}
}