个人在学习Spring Security过程中的笔记
认证,授权,针对常见工具保护,底层是过滤器链。



UsernamePasswordAuthenticationFilter: 用来根据传递进来的用户名及密码进行用户认证。
ExceptionTranslationFilter: 允许将AccessDeniedException和AuthenticationException转换为HTTP响应,这两个异常在Spring Security中分别代表权限异常和认证异常。
FilterSecurityInterceptor: 对HttpServletRequests进行权限校验。它作为Spring Security中的一员插入到FilterChainProxy中。

UsernamePasswordAuthenticationFilter是个过滤器,其父类AbstractAuthenticationProcessingFilter实现了接口Filter的相关方法。
这里查看AbstractAuthenticationProcessingFilter中的doFilter方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 1. 得到request和response
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 2. 判断是否是post请求且url为'/login'
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 3. 调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法得到认证后的信息
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 4. session操作
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// 5.1. 由于程序错误抛出异常,执行认证失败方法
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 5.2. 由于认证失败,执行认证失败方法
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// 5.3. 认证成功,下一个过滤器
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 5.3.1. 执行认证成功方法,这里会把认证信息(Authentication)设置到SecurityContext中,然后执行handler中的success方法(响应成功)
successfulAuthentication(request, response, chain, authResult);
}UsernamePasswordAuthenticationFilter的attemptAuthentication认证方法username和password参数public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 判断是否是POST请求,这里postOnly为true
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 2. 获取请求中的username和passowrd参数
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 3. 创建UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 4. 调用AuthenticationManager(这里是ProviderManager)的authenticate方法,得到认证结果Authentication
return this.getAuthenticationManager().authenticate(authRequest);
}ProviderManager的authenticate认证方法ProviderManager是AuthenticationManager的实现类,ProviderManager的释义如下:
原文:
AuthenticationManageris the API that defines how Spring Security’s Filters perform authentication. TheAuthenticationthat is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’sFilterss) that invoked theAuthenticationManager. If you are not integrating with Spring Security’sFilterss you can set theSecurityContextHolderdirectly and are not required to use anAuthenticationManager. While the implementation ofAuthenticationManagercould be anything, the most common implementation isProviderManager. 中文: AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即Spring Security的过滤器)在SecurityContextHolder上设置返回的身份验证。如果未与Spring Security的过滤器集成,则可以直接设置SecurityContextHolder,无需使用AuthenticationManager。(即可以绕过manager自定义) 虽然AuthenticationManager的实现可以是任何形式,但最常见的实现是ProviderManager。
ProviderManager
原文
ProviderManageris the most commonly used implementation ofAuthenticationManager.ProviderManagerdelegates to aListofAuthenticationProviders. EachAuthenticationProviderhas an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstreamAuthenticationProviderto decide. If none of the configuredAuthenticationProviders can authenticate, then authentication will fail with aProviderNotFoundExceptionwhich is a specialAuthenticationExceptionthat indicates theProviderManagerwas not configured to support the type ofAuthenticationthat was passed into it. 大概意思就是说ProviderManager是AuthenticationManager最常见的实现类,保存在ProviderManager的每一个AuthenticationProvider只要能够支持本次验证逻辑(support),则都会进行身份认证,并且即使上游的AuthenticationProvider认证成功,下游的AuthenticationProvider也可以接着自己的认证逻辑。如果所有的AuthenticationProvider列表都不能够认证,则会抛出特殊的ProviderNotFoundException异常。
整体流程
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 得到AuthenticationProvider,依次认证
// 注意在这里第一次获取的是只有AnonymousAuthenticationProvider,这个provider不能支持认证
// 之后会由于result == null && parent != null,会调用父provider(也是ProviderManager类,是不同的对象)的authenticate方法
// 这是返回的getProviders()是DaoAuthenticationProvider,这个就支持认证了
for (AuthenticationProvider provider : getProviders()) {
// 是否支持本次认证
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 调用provider认证方法,判断认证结果
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
// 如果没有provider可以认证,则尝试父manager的provider
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 异常处理,认证完的话要把token里面的密码信息等抹除
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
// 如果子和父manager没有provider可以处理,则抛出异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}是DaoAuthenticationProvider的authenticate方法
这里不贴代码了,整体流程如下:
UserDetails用户信息,没的话则现场构建一个UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法构建UserDetailsService的UserDetails loadUserByUsername(String username)方法得到一个UserDetails实现类,默认是Spring Security中的User类,而UserDetailsService的默认实现类为InMemoryUserDetailsManager,其是把用户信息存在内存中,采用的是HashMapUserDetails是否禁用了,若禁用的话抛出CredentialsExpiredException异常,否则继续UserDetails是否密码正确,错误的话则抛出BadCredentialsException异常,否则继续UserDetails返回AuthenticationUserDetails和UserDetailsService
UserDetails: SpringSecurity中用来认证的接口,可以通过实现该接口来实现自定义
UserDetailsService: 该接口只有一个loadUserByUsername方法,用来根据用户名获取一个UserDetails的实现类,用来比对用户输入的密码认证

这里仅展示自定义部分
UserDetailspublic class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 907051613876467178L;
// 业务需求的用户DTO
private LoginUser loginUser;
// 用户权限
private List<Permission> permissions;
public UserDetailsImpl(LoginUser loginUser) {
this.loginUser = loginUser;
}
public UserDetailsImpl(LoginUser loginUser, List<Permission> permissions) {
this.loginUser = loginUser;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.stream()
.filter(permission -> permission != null && !Objects.equals(permission.getValue(), ""))
.map(permission -> new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return loginUser.getPassword();
}
@Override
public String getUsername() {
return loginUser.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return loginUser.getStatus().equals(0);
}
public LoginUser getLoginUser() {
return loginUser;
}
public void setLoginUser(LoginUser loginUser) {
this.loginUser = loginUser;
}
}UserDetailsService@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private UserCacheService userCacheService;
@Autowired
private PermissionService permissionService;
// 从mysql及redis获取用户信息
private LoginUser getLoginUser(String username) {
LoginUser loginUser = userCacheService.getLoginUser(username);
if(!Objects.isNull(loginUser)) {
return loginUser;
}
// 这里若还是查不到用户,则loginUser还是为空
User user = userService.getUserByUserName(username);
if(!Objects.isNull(user)) {
loginUser = UserConvertor.toLoginUser(user);
userCacheService.setLoginUser(loginUser);
}
return loginUser;
}
// 从mysql及redis获取权限信息
private List<Permission> getPermissions(Long userId) {
List<Permission> permissions = userCacheService.getUserPermissions(userId);
if (!Objects.isNull(permissions)) {
return permissions;
}
permissions = permissionService.getPermissions(userId);
if (!Objects.isNull(permissions)) {
userCacheService.setUserPermissions(userId, permissions);
}
return permissions;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.用户信息
LoginUser loginUser = getLoginUser(username);
if (Objects.isNull(loginUser)) {
throw new UsernameNotFoundException("用户不存在");
}
// 2.权限
List<Permission> permissions = getPermissions(loginUser.getId());\
// 封装UserDetails返回
return new UserDetailsImpl(loginUser, permissions);
}
}service 自定义SpringSecurity登陆@Service
public class UserSecurityServiceImpl implements UserSecurityService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public UserDetailsImpl login(String username, String password) {
// 这里应用了UsernamePasswordAuthenticationToken的带密码构造函数
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, password);
// 调用manager的authenticate
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (Objects.isNull(authenticate) || !authenticate.isAuthenticated()) {
throw new UsernameNotFoundException("用户不存在");
}
UserDetailsImpl userDetails = (UserDetailsImpl) authenticate.getPrincipal();
// 得到认证后的用户
// TODO: 其他操作
return userDetails;
}
}public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisService redisService;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 从header获取token
String token = request.getHeader(tokenHeader);
if(!StringUtils.hasText(token)) {
// 放行,后续security根据配置和context检测
filterChain.doFilter(request, response);
return ;
}
// 2. jwt解析过期
if (jwtUtils.isTokenExpired(token)) {
throw new RuntimeException("用户登陆过期");
}
// 3. 解析token信息,获取信息后注入security
String username = jwtUtils.getUserNameFromToken(token);
// token认证通过了,直接调用loadUserByUsername得到用户信息,用来注入context
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 应用了UsernamePasswordAuthenticationToken的不带密码构造函数,用来注入SecurityContext
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
}
}Security配置@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy((SessionCreationPolicy.STATELESS))
.and()
.authorizeRequests()
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
}
@Bean
public PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// 注意:这里这么些是因为过滤器的初始化的时机要比Spring初始化bean靠前,不注入bean则无法使用@Value获取配置文件的值
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}三更Spring Security: https://www.bilibili.com/video/BV1mm4y1X7Hc?spm_id_from=333.337.search-card.all.click
Spring Security官网: https://docs.spring.io/spring-security/reference/index.html