本篇文章介绍如何在项目中引入Spring security,以及一些伪代码
1、先引入Jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、当我们引入上面的jar包,spring security就会启动默认配置,包括登录页面,登录账号密码,拦截路径,成功或者失败返回的内容都配置好了,如果我们实现自定义的,就要修改他的配置。先把spring security默认配置都给他换了
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//认证成功后处理的bean
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
//认证失败处理的bean
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
//退出登录成功处理的bean
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
//未登录处理
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
//用户获取用户信息的bean
@Autowired
private UserDetailsService userDetailsService;
//token处理的bean,为了要有这个,因为当用户登录成功之后,每次请求只带token我们要知道谁谁
@Autowired
private TokenFilter tokenFilter;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
//加密类
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
log.info("用户登录拦截");
http.csrf().disable().cors();//CSRF跨站请求伪造
http.authorizeRequests()
.antMatchers("/health").permitAll()
.antMatchers("/sysUserMenu/**").hasAnyAuthority("ROLE_ADMIN")
//需要对外暴露哪些接口可以在这里设置
.anyRequest().authenticated();
//解决不允许显示在iframe的问题
http.headers().frameOptions().disable();
http.headers().cacheControl();
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//用来解决匿名用户访问无权限资源时的异常
http.formLogin().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
//用来解决认证过的用户访问无权限资源时的异常
//http.formLogin().and().exceptionHandling().accessDeniedHandler(new CustomAccessDeineHandler());
//退出登录处理handler
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
//下面2行代码的作用是什么都是向filter里面添加过滤器,但是添加的filter类型各不一样。
//前置过滤器,所有的请求一进来,先进到这个过滤器,UsernamePasswordAuthenticationFilter也就是spirng security第一个过滤器
http.addFilterAt(tokenFilter, UsernamePasswordAuthenticationFilter.class);
//后置过滤器,这个包含,处理成功,处理失败,包括账户认证
http.addFilterBefore(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Bean
public MyAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
MyAuthenticationFilter filter = new MyAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());//添加AuthenticationManager账户认证,这个类很重要。
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);//成功处理器
filter.setAuthenticationFailureHandler(authenticationFailureHandler);//失败处理器
return filter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)//配置UsernamePasswordAuthenticationFitel过滤器
.passwordEncoder(bCryptPasswordEncoder());//配置passwordEncoder用于密码的加密和比对,认证用户,认证用户的密码和数据是否一致
}
}
上面代码做的几个事情
1、配置那些请求需要拦截,那些不需要拦截
2、往Spring Security拦截器链中加入自定义的拦截器(加入了2个拦截器MyAuthenticationFilter、TokenFilter)
3、在拦截器中配置认证成功,认证失败的处理逻辑。
4、配置获取用户信息处理的bean UserDetailService
备注:上面2个拦截器分别是在首次登录通过处理前端穿的username和password来做处理,第二个是拦截器是在后续登录只传token来做处理。
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过username得到对象,查数据库也好,读配置也好
UserDetails userDetails = new UserDetails();
//设置用户权限
userDetails.setRoles(roles);
return userDetails;
}
}
实现UserDetailsService用来获取UserDetails,为什么要这样做。因为Spring Security里面有个AuthenticationManager他会根据前端穿的密码和从数据库通过username查到的密码进行对比认证。下面引入一段源码。
return this.getAuthenticationManager().authenticate(authRequest);
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
if (result == null && parent != null) {
result = parentResult = parent.authenticate(authentication);
}
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//重点在这里会调用我们实现的UserDetailsService
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
下面引入第一个过滤器TokenFilter,主要是在用户已经登录成功之后,后续在请求进来,那么穿的就不是账号密码而是token。这个时候我们就要把token转成UserDetails就要用到这个过滤器。
@Component
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//获取token
String token = this.getToken(request);
logger.info("用户校验token"+token);
if (StringUtils.isNotBlank(token)) {
//通过token的到用户信息,可以基于Redis来做
UserDetails userDetails = getUserDetailsByToken(token);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
下面是第二个过滤器,主要用于从请求中获取username和password,然后把他转成UsernamePasswordAuthenticationToken交给AuthenticationManager来认证。
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public MyAuthenticationFilter() {
super(new AntPathRequestMatcher("/xtapi/users/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//判断请求方式为POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("xxx");
}
//获取请求参数类型
String contentType = request.getContentType();
if(!contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
&& !contentType.equals(MediaType.APPLICATION_JSON_VALUE)){
//如果请求类型是非postJson方式
throw new AuthenticationServiceException("xxx");
}
//认证凭证对象
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()){
ObjectMapper mapper = new ObjectMapper();
Map<String,String> map = mapper.readValue(is, Map.class);
//得到自定义的参数
authRequest = new UsernamePasswordAuthenticationToken(map.get("userName"),map.get("password"));
}catch (Exception e){
authRequest = new UsernamePasswordAuthenticationToken("","");
}finally {
setDetails(request,authRequest);
//开始认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
下面是认证成功或者认证失败的处理bean
@Configuration
public class SecurityHandlerConfig {
/**
* 登陆成功,返回Token
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Map<String,Object> map = new HashMap<>();
//通过userDetails生成token并把token存入缓存中
Token token = saveToken(userDetails);
map.put("token",token.getToken());
ResponseUtil.responseJson(request,response, HttpStatus.OK.value(), map);
}
};
}
/**
* 登陆失败
*/
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String msg = null;
if (exception instanceof BadCredentialsException) {
msg = "密码错误";
} else {
msg = exception.getMessage();
}
ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
ResponseUtil.responseJson(request,response, HttpStatus.UNAUTHORIZED.value(), info);
}
};
}
/**
* 未登录,返回401
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseInfo info = new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", "请先登录");
ResponseUtil.responseJson(request,response, HttpStatus.FORBIDDEN.value(), info);
}
};
}
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseInfo info = new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", "没有访问权限!");
ResponseUtil.responseJson(request,response, HttpStatus.FORBIDDEN.value(), info);
}
};
}
/**
* 退出处理
*/
@Bean
public LogoutSuccessHandler logoutSussHandler() {
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");
//通过token得到得到缓存
String token = getUserDetailsByToken(request);
//讲token从缓存中移除
this.deleteToken(token);
ResponseUtil.responseJson(request,response, HttpStatus.OK.value(), info);
}
};
}
}