【springsecurity使用】

springsecurity使用

  • 简介
  • 原理
    • SecurityContextPersistenceFilter :
    • UsernamePasswordAuthenticationFilter
    • ExceptionTranslationFilter:
    • FilterSecurityInterceptor:
  • 过滤链构造
  • 认证
      • 过滤器UsernamePasswordAuthenticationFilte
        • UsernamePasswordAuthenticationFilte内认证方法
          • authenticate方法
            • authenticate-第一步调retrieveUser查询用户信息
            • authenticate-第二步调additionalAuthenticationChecks比对密码
            • authenticate-第三步调createSuccessAuthentication设置认证成功
        • UsernamePasswordAuthenticationFilte内认证成功方法
      • 自定义实现认证
  • 授权
    • 原理
  • 异常处理机制

简介

springsecurity是一个功能强大且高度可定制的身份验证和访问控制框架。包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证用户能否访问该系统,一般要求用户提供用户名和密码。用户授权指的是验证某个用户是否有权限执行某个操作,一般来说,系统会为不同的用户分配不同的角色,对应一系列的权限。

原理

Spring Security对Web资源的保护是靠Filter实现的。当初始化Spring Security时,会创建一个名为 springSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过过滤器再到api业务逻辑,下图是Spring Security过滤器链结构图:
【springsecurity使用】_第1张图片

SecurityContextPersistenceFilter :

整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext。

UsernamePasswordAuthenticationFilter

负责处理我们在登陆页面填写了用户名密码后的登陆请求。其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。

ExceptionTranslationFilter:

处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:

负责权限校验的过滤器。

过滤链构造

【springsecurity使用】_第2张图片

    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()  // 所有请求需要身份认证
                .and().logout()
                .logoutUrl("/xxx")
                .logoutSuccessHandler(logoutSuccessHandler);
        http.headers().frameOptions().sameOrigin();
        http.securityContext().securityContextRepository(contextRepository);
        http.addFilterBefore(verifyCodeFilter, SecurityContextPersistenceFilter.class);
        http.addFilterBefore(skipFilter, SecurityContextPersistenceFilter.class);
        http.addFilterAfter(ExceptionFilter(), TumsExceptionTranslationFilter.class);
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(appLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterAt(logoutFilter(), LogoutFilter.class);
        http.addFilterBefore(tumsUserExpireCheckFilter, FilterSecurityInterceptor.class);
    }

在指定的beforeFilter之前加入filter,这里的comparator是内置的一个用于比较注册顺序的一个类。

public HttpSecurity addFilterBefore(Filter filter,
                                    Class<? extends Filter> beforeFilter) {
    comparator.registerBefore(filter.getClass(), beforeFilter);
    return addFilter(filter);
}

认证

【springsecurity使用】_第3张图片

过滤器UsernamePasswordAuthenticationFilte

进入过滤器UsernamePasswordAuthenticationFilte它继承 AbstractAuthenticationProcessingFilter,执行该方法,解析前端出传的用户名和密码。

UsernamePasswordAuthenticationFilte内认证方法
    //认证
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        ...
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        
    //认证成功后
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    ...
        SecurityContextHolder.getContext().setAuthentication(authResult);
    ...
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    } 
    

过滤器第一步进行认证执行attemptAuthentication方法,用户名和密码封装成UsernamePasswordAuthenticationToken,此时没认证。

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null);
    //用户
    this.principal = principal;
    //密码
    this.credentials = credentials;
    this.setAuthenticated(false);
}
authenticate方法

然后继续调用AbstractUserDetailsAuthenticationProvider接口的authenticate方法去认证,第一步查询用户信息调retrieveUser方法。

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
    String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    ...
            //查询用户信息
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
    ...
            //比对密码
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    ...
    //设置认证成功存数据
    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
authenticate-第一步调retrieveUser查询用户信息

retrieveUser通过loadUserByUsername方法查询用户信息。将用户信息包含权限信息,然后封装成UserDetails返回。
我们可以实现UserDetailsService接口,自定义获取用户信息。

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    UserDetails loadedUser;
    ...
        loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    ...
}

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
authenticate-第二步调additionalAuthenticationChecks比对密码

回到authenticate方法,第二步用将查询好封装的UserDetails与前端传入的密码比对,调additionalAuthenticationChecks。
PasswordEncode:数据加密接口

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
authenticate-第三步调createSuccessAuthentication设置认证成功

正确则回到authenticate方法,第三步设置认证成功,调createSuccessAuthentication。

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
       UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
       result.setDetails(authentication.getDetails());
       return result;
   }

这里的UsernamePasswordAuthenticationToken构造方法如下,则从未认证到已认证。

   public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
     super(authorities);
     this.principal = principal;
     this.credentials = credentials;
     super.setAuthenticated(true);
 }
UsernamePasswordAuthenticationFilte内认证成功方法

认证成功后,过滤器进入第二步调successfulAuthentication,将认证信息放入SecurityContext。
同时可以重写该方法,根据需求设置session等。

   protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
       if (this.logger.isDebugEnabled()) {
           this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
       }

       SecurityContextHolder.getContext().setAuthentication(authResult);
       this.rememberMeServices.loginSuccess(request, response, authResult);
       if (this.eventPublisher != null) {
           this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
       }

       this.successHandler.onAuthenticationSuccess(request, response, authResult);
   }

自定义实现认证

1.自定义类继承AbstractAuthenticationProcessingFilter,重写attemptAuthentication方法解析前端出传的用户名和密码,认证成功successfulAuthentication方法。

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain
            chain, Authentication auth) throws IOException, ServletException {
        String username = (String) auth.getPrincipal();
        ...
        TumsSession session = sessionManager.doGetSession(request, response);
        session.setUser(userDao.findByUsername(username));
        userManager.userInformationSaveToSession(session, username);
        super.successfulAuthentication(request, response, chain, auth);
        msLog.oper("系统", "登入", "{}登录成功", username);
    }

2.userDetailService
自定义类实现接口userDetailService,重写loadUserByUsername方法,通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息包括权限信息,然后封装成UserDetails,并返回。

如:(UserInfo 实现UserDetails接口)

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo user = userDao.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return user;
}

总结:
1.没有认证过的请求重定向到登陆页面进行登录
2.用户输入用户名和密码后请求认证。在继承AbstractAuthenticationProcessingFilter的自定义类解析出用户名和密码封装成UsernamePasswordAuthenticationToken对象,进一步向数据库或缓存查询用户信息,校验密码。
3.校验成功则会保存返回的Authentication到SecurityContext,认证成功。否则会抛出异常,常做的处理是重定向到登陆页面。
4.认证成功后,用户访问资源时会进行权限鉴定。

授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。

​ 所以我们在项目中只需要
1.把当前登录用户的权限信息也存入Authentication。

public class UserInfo implements UserDetails {
    ...
    @Override
    public  Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

​ 2.然后设置我们的资源所需要的权限即可。

//1.通过注解
    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    public String hello(){
        return "hello";
    }
    
//2.通过配置类
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .antMatchers("/testCors").hasAuthority("system:dept:list222")
            ...
     }

原理

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        //用户拥有的权限
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;
        
        //判断是否包含
        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }

    private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            this.roles = new HashSet();
            //调用重写的方法
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }

            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }

        return this.roles;
    }

异常处理机制

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

​ 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

​ 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

​ 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

//权限异常
public class TumsAccessDeniedHandler implements AccessDeniedHandler {

    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException
            accessDeniedException)
            throws IOException {
        ApiResult result = new ApiResult(UserErrCode.USER_NO_PERMISSION);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().append(new ObjectMapper().writeValueAsString(result));
    }
}

你可能感兴趣的:(servlet,java,前端)