依赖
org.springframework.boot
spring-boot-starter-security
如果要禁用,启动类上配置
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
或
@EnableAutoConfiguration(exclude = {SecurityAutoConfigurati on.class})
配置管理类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // httpbasic()
.and()
.authorizeRequests() // 请求授权
.anyRequest() // 任何请求,都需要身份认证
.authenticated();
}
}
过滤器链
链中的过滤器会挨个检查需不需要处理,如果需要处理则处理,不需要的话放给下一个过滤器处理
UserNamePasswordAuthenticationFilter(表单登录) ---> BasicAuthencationFilter(basic方式登录) --> 自定义的过滤器 ---> ExceptionTranslationFilter ----> FilterSecurityInterceptor
FilterSecurityInterceptor
是链路的最后一个,用于依据代码配置判断用户能不能访问内容
ExceptionTranslationFilter
捕获到FilterSecurityInterceptor抛出的异常,引导用户作出不同的操作来
只有绿色的filter才能修改或增加
自定义用户认证逻辑
1、处理用户信息获取逻辑
Spring Security封装在了接口里面
实现此接口,从数据库或者别的地方获取用户信息,并封装在UserDetails对象里面,然后Security进行处理和校验,如果检验通过了,就把用户信息放在session里面。
public interface UserDetailService {
UserDetails loadUserByUsername(String username) throws UserNotFoundException;
}
@Slf4j
@Component
public class PCUserDetailsService implements UserDetailsService {
@Autowired
private Userservice userservice;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserDetail(username);
/**
* 因为UserDetails是一个接口,所以返回sucurity的user实现类
* 参数1:username 参数2:密码 参数3:权限
*/
return new org.springframework.security.core.userdetails.User(username, user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("test"));
}
}
2、处理用户校验逻辑、处理密码加密解密
UserDetails
接口除了封装 username\password\权限之外,还封装了几个别的参数,如校验过期、冻结、可用性等。
- isAccountNonExpired() 返回ture,账户没有过期
- isAccountNonLocked() 返回ture,没有锁定
- isCredentialsNonExpired() 密码是否过期
- isEnabled() 是否可用,是否被逻辑删除等
第二个参数是密码,不要直接返回数据库以及被加密的密码即可
new org.springframework.security.core.userdetails.User(username, "123456",
true, true, true, true, #这四个,根据需求自定义判断逻辑
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
配置PasswordEncoder。在保存用户之前,需要encode一下密码再存库
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
3、定义认证接口和登录入口
loginPage 表示需要认证时,自动转发到这个接口(前后一体也可以是页面)
loginProcessingUrl 表示Spring Security 认为配置的/login/in 是用UsernamePasswordAuthenticationFilter
http
.formLogin()
// 指定登录页面所在的url,这个url可以自定义一个controller
.loginPage("/authentication/require")
// 默认是upaf处理的“/login”链接的内容,我这里自定义一个"/api/login/in",filter知道是表单提交
.loginProcessingUrl("/login/in")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.authorizeRequests() // 请求授权
.antMatchers("/item*", "/login/in",
securityProperties.getBrowser().getLoginPage()).permitAll() // 表示不需要认证
.anyRequest() // 任何请求,都需要身份认证
.authenticated()
.and().csrf().disable();
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 当需要身份验证时,跳转到这个接口(由loginPage() 配置此链接 )
* @param request
* @param response
* @return
*/
@GetMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public ResponseVO requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
/* 判断请求是从哪里来的,判断需不要进行验证
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (Objects.nonNull(savedRequest)) {
String targetUrl = savedRequest.getRedirectUrl();
log.info("引发跳转的请求是:" + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return ResponseVO.error(MsgExample.UNAUTHORIZED);*/
/** 个人理解:前后端分离项目,配置login页面指向了这个接口,
* 我们应该在前端response拦截器中判断返回结果,
* 写固定内容,如果需要验证,则直接跳转到登录页面
*/
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (Objects.nonNull(savedRequest)) {
String targetUrl = savedRequest.getRedirectUrl();
log.info("引发跳转的请求是:" + targetUrl);
}
return ResponseVO.error(MsgExample.UNAUTHORIZED);
}
4、自定义认证成功和失败处理器(需要注册到config配置中,如3)
@Slf4j
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* @param httpServletRequest
* @param httpServletResponse
* @param authentication 核心接口,封装认证信息:认证请求的ip,请求的session信息
* 封装认证成功信息:UserDetails用户信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
log.info("login success");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
String s = ResponseVO.succ(objectMapper.writeValueAsString(authentication)).toString();
out.write(s);
out.flush();
out.close();
}
}
@Slf4j
@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
log.info("login failure");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
String s = ResponseVO.error(new CustomException(MsgExample.LOGIN_ERROR), e.getMessage()).toString();
out.write(s);
out.flush();
out.close();
}
}
认证流程
认证流程如下:1.UsernamePasswordAuthenticationFilter 获取请求中的username和password,封装为一个UsernamePasswordAuthenticationToken, 这个token类实现了Authenticator接口,是封装认证信息的。此外,filter还将request中的session信息和请求信息都交给了这个token。 最后将token交给了AuthenticationManager
2.AuthenticationManager和Shiro中的SecurityManager类似,它不是用来验证的,而是管理了AuthenticationProvider来处理认证逻辑。
3.不同的认证逻辑需要不同的Provider,如用户名密码登录、微信登录等、QQ登录等。Manager遍历所有的Provider看哪种认证逻辑符合要求。
- 默认用户名密码登录使用DaoAuthenticationProvider 来进行验证,它的父类实现了authenticate方法,而daoProvider则是实现了一个它里面的retrieveUser() 方法,这个方法就是从自定义UserDetailsService中获取数据库中的用户信息。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
authenticate方法之后会使用this.preAuthenticationChecks.check(user); 方法进行预检查,检查锁定、删除、过期的boolean。其后的additionalAuthenticationChecks附加检查密码和密码过期,都通过之后我们就认为认证成功了。
认证通过后,执行createSuccessAuthentication创建一个新的Authentication对象,而且它现在可以获取到从UserDetailsService中获取的用户权限信息了,也会封装进里面。并设置了authenticated的boolean为ture
回到最开始的UsernamePasswordAuthenticationFilter 之后,它的父类的方法将结果信息交给了successhandler处理。如果中途有错,也会被捕获交给failureHandler处理
认证结果如何在多个请求之间共享
- SecurityContextPersistenceFilter
- SecurityContextHolder
- SecurityContext
1.认证完成掉用successHandler之前,会将认证成功的Authentication放在SecurityContext里面,它就是对Authentication的一层封装,只是重写了equels和hashcode方法
2.SecurityContextHolder是对ThreadLocal的封装,请求和响应都是一个线程里完成的,当前会话都用一个SecurityContextHolder处理了。
3.SecurityContextPersistenceFilter,在整个过滤器链的最前面,因此请求先给他,最后返回离开他,检查请求中session是否有SecurityContext,如果有就放在SecurityContextHolder中。最后返回检查线程,如果线程有SecurityContext就拿出来放在Session里面。这样每个请求在会话里拿到的数据就一样了。
这样在任何逻辑位置,都可以从SecurityContextHolder.getContext() 获取到Authentication用户信息。
@GetMapping
public String test(){
String result = SecurityContextHolder.getContext().getAuthentication().toString();
return result;
}
或者
@GetMapping
public String test2(Authentication authentication){
return authentication.toString();
}