SpringSecurity搭建

依赖

   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才能修改或增加

链路图.png

自定义用户认证逻辑

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();
    }
}

认证流程

认证流程.png

认证流程如下:1.UsernamePasswordAuthenticationFilter 获取请求中的username和password,封装为一个UsernamePasswordAuthenticationToken, 这个token类实现了Authenticator接口,是封装认证信息的。此外,filter还将request中的session信息和请求信息都交给了这个token。 最后将token交给了AuthenticationManager
2.AuthenticationManager和Shiro中的SecurityManager类似,它不是用来验证的,而是管理了AuthenticationProvider来处理认证逻辑。
3.不同的认证逻辑需要不同的Provider,如用户名密码登录、微信登录等、QQ登录等。Manager遍历所有的Provider看哪种认证逻辑符合要求。

Provider.png
  1. 默认用户名密码登录使用DaoAuthenticationProvider 来进行验证,它的父类实现了authenticate方法,而daoProvider则是实现了一个它里面的retrieveUser() 方法,这个方法就是从自定义UserDetailsService中获取数据库中的用户信息。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  1. authenticate方法之后会使用this.preAuthenticationChecks.check(user); 方法进行预检查,检查锁定、删除、过期的boolean。其后的additionalAuthenticationChecks附加检查密码和密码过期,都通过之后我们就认为认证成功了。

  2. 认证通过后,执行createSuccessAuthentication创建一个新的Authentication对象,而且它现在可以获取到从UserDetailsService中获取的用户权限信息了,也会封装进里面。并设置了authenticated的boolean为ture

  3. 回到最开始的UsernamePasswordAuthenticationFilter 之后,它的父类的方法将结果信息交给了successhandler处理。如果中途有错,也会被捕获交给failureHandler处理

认证结果如何在多个请求之间共享

  • SecurityContextPersistenceFilter
  • SecurityContextHolder
  • SecurityContext
PersistenceFilter.png

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();
}

你可能感兴趣的:(SpringSecurity搭建)