zl程序教程

您现在的位置是:首页 >  工具

当前栏目

Security使用笔记

笔记 使用 Security
2023-06-13 09:11:07 时间

Spring Security使用笔记

个人在学习Spring Security过程中的笔记

1. Spring Security作用

认证,授权,针对常见工具保护,底层是过滤器链。

2. 简单应用

2.1. 一些过滤器

UsernamePasswordAuthenticationFilter: 用来根据传递进来的用户名及密码进行用户认证。

ExceptionTranslationFilter: 允许将AccessDeniedExceptionAuthenticationException转换为HTTP响应,这两个异常在Spring Security中分别代表权限异常和认证异常。

FilterSecurityInterceptor: 对HttpServletRequests进行权限校验。它作为Spring Security中的一员插入到FilterChainProxy中。

2.2. UsernamePasswordAuthenticationFilter的流程

  1. 由于UsernamePasswordAuthenticationFilter是个过滤器,其父类AbstractAuthenticationProcessingFilter实现了接口Filter的相关方法。

​ 这里查看AbstractAuthenticationProcessingFilter中的doFilter方法:

  1. 得到httpRequest和httpResponse
  2. 判断request是否是post请求且url为’/login’
  3. 调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法得到认证后的信息
  4. session操作
  5. 执行认证成功或认证失败的方法
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);
}
  1. UsernamePasswordAuthenticationFilterattemptAuthentication认证方法

  1. 判断是否是POST请求
  2. 是的话就拿出request中的usernamepassword参数
  3. 接着根据username和password创建UsernamePasswordAuthenticationToken,(扩展:这里要注意的UsernamePasswordAuthenticationToken有两个构造函数,一个是带密码的,一个是不带密码的,带密码的构造函数一般用来登陆时候用,主要用来认证,而不带密码的构造函数一般用来注入进SecurityContext的,主要用来权限校验,如上面的successfulAuthentication方法中就是用的就是不带密码的构造函数)
  4. 调用AuthenticationManager(这里是ProviderManager)的authenticate方法
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);
}
  1. ProviderManagerauthenticate认证方法

ProviderManagerAuthenticationManager的实现类,ProviderManager的释义如下:

原文: AuthenticationManager is the API that defines how Spring Security’s Filters perform authentication. The Authentication that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’s Filterss) that invoked the AuthenticationManager. If you are not integrating with Spring Security’s Filterss you can set the SecurityContextHolder directly and are not required to use an AuthenticationManager. While the implementation of AuthenticationManager could be anything, the most common implementation is ProviderManager. 中文: AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即Spring Security的过滤器)在SecurityContextHolder上设置返回的身份验证。如果未与Spring Security的过滤器集成,则可以直接设置SecurityContextHolder,无需使用AuthenticationManager。(即可以绕过manager自定义) 虽然AuthenticationManager的实现可以是任何形式,但最常见的实现是ProviderManager。

ProviderManager

原文 ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProviders. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream AuthenticationProvider to decide. If none of the configured AuthenticationProviders can authenticate, then authentication will fail with a ProviderNotFoundException which is a special AuthenticationException that indicates the ProviderManager was not configured to support the type of Authentication that was passed into it. 大概意思就是说ProviderManagerAuthenticationManager最常见的实现类,保存在ProviderManager的每一个AuthenticationProvider只要能够支持本次验证逻辑(support),则都会进行身份认证,并且即使上游的AuthenticationProvider认证成功,下游的AuthenticationProvider也可以接着自己的认证逻辑。如果所有的AuthenticationProvider列表都不能够认证,则会抛出特殊的ProviderNotFoundException异常。

整体流程

  1. 轮询本manager的provider列表,认证
  2. 要是provider列表没有可以认证的,则调用父manager的authenticate继续尝试
  3. 如果认证成功了,则要抹除token里面的密码信息
  4. 如果子和父manager都没有provider可以认证,则抛出ProviderNotFoundException
  5. 返回认证信息Authentication
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;
}
  1. 是DaoAuthenticationProviderauthenticate方法 这里不贴代码了,整体流程如下:
    1. 从缓存拿UserDetails用户信息,没的话则现场构建一个
    2. 缓存miss,调用UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法构建
    3. retrieveUser()中调用UserDetailsServiceUserDetails loadUserByUsername(String username)方法得到一个UserDetails实现类,默认是Spring Security中的User类,而UserDetailsService的默认实现类为InMemoryUserDetailsManager,其是把用户信息存在内存中,采用的是HashMap
    4. 检查UserDetails是否禁用了,若禁用的话抛出CredentialsExpiredException异常,否则继续
    5. 检查UserDetails是否密码正确,错误的话则抛出BadCredentialsException异常,否则继续
    6. 根据UserDetails返回Authentication
  2. UserDetailsUserDetailsService UserDetails: SpringSecurity中用来认证的接口,可以通过实现该接口来实现自定义 UserDetailsService: 该接口只有一个loadUserByUsername方法,用来根据用户名获取一个UserDetails的实现类,用来比对用户输入的密码认证
  3. 整体流程

3. 实战

这里仅展示自定义部分

  1. 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;
    }
}
  1. 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);
    }
}
  1. 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;
    }

}
  1. jwt登陆过滤器 用来检查用户登陆态
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);
    }
}
  1. 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();
    }
}

4. 参考

三更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