个人在学习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
的释义如下:
原文:
AuthenticationManager
is the API that defines how Spring Security’s Filters perform authentication. TheAuthentication
that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’sFilters
s) that invoked theAuthenticationManager
. If you are not integrating with Spring Security’sFilters
s you can set theSecurityContextHolder
directly and are not required to use anAuthenticationManager
. While the implementation ofAuthenticationManager
could be anything, the most common implementation isProviderManager
. 中文: AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即Spring Security的过滤器)在SecurityContextHolder上设置返回的身份验证。如果未与Spring Security的过滤器集成,则可以直接设置SecurityContextHolder,无需使用AuthenticationManager。(即可以绕过manager自定义) 虽然AuthenticationManager的实现可以是任何形式,但最常见的实现是ProviderManager。
ProviderManager
原文
ProviderManager
is the most commonly used implementation ofAuthenticationManager
.ProviderManager
delegates to aList
ofAuthenticationProvider
s. EachAuthenticationProvider
has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstreamAuthenticationProvider
to decide. If none of the configuredAuthenticationProvider
s can authenticate, then authentication will fail with aProviderNotFoundException
which is a specialAuthenticationException
that indicates theProviderManager
was not configured to support the type ofAuthentication
that 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
,其是把用户信息存在内存中,采用的是HashMap
UserDetails
是否禁用了,若禁用的话抛出CredentialsExpiredException
异常,否则继续UserDetails
是否密码正确,错误的话则抛出BadCredentialsException
异常,否则继续UserDetails
返回Authentication
UserDetails
和UserDetailsService
UserDetails
: SpringSecurity中用来认证的接口,可以通过实现该接口来实现自定义
UserDetailsService
: 该接口只有一个loadUserByUsername
方法,用来根据用户名获取一个UserDetails
的实现类,用来比对用户输入的密码认证
这里仅展示自定义部分
UserDetails
public 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