美文网首页
SpringSecurity

SpringSecurity

作者: dwwl | 来源:发表于2019-10-25 17:15 被阅读0次

    SpringSecurity

    github:https://github.com/Fly0708/securityDemo.git
    一、框架的基础使用

    当pom文件中加入Spring Security依赖时,默认开启权限认证

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    

    自定义SecurityConfig配置类

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private SuccessHandler successHandler;
    
        @Autowired
        private FailureHandler failureHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                //basic和表单两种方式,formLogin的拦截器UsernamePasswordAuthenticationFilter
                    .formLogin()
                    .loginPage("/login")
                //自定义登录url(覆盖AbstractAuthenticationProcessingFilter中的RequestMatcher),post方式,
                    .loginProcessingUrl("/login")
                //覆盖默认成功处理器,默认实现跳转上一url,先返回登录用户信息json
                    .successHandler(successHandler)
                //覆盖默认失败处理器,默认实现跳转登录页面loginPage,先返回失败信息
                    .failureHandler(failureHandler)
                    .and()
                //对授权的路径进行配置,
                    .authorizeRequests()
                    .antMatchers("/login.html","/login").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .csrf().disable();
        }
    
        //spring security内置的加密器,加密中的字符串中有salt,每次加密的salt均不一样,所以对同一内容加密后的结果是不同的。
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    

    SuccessHandler.java

    @Component
    public class SuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            ObjectMapper objectMapper = new ObjectMapper();
    
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }
    }
    

    FailureHandler.java

    @Component
    public class FailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(500);
            ObjectMapper objectMapper = new ObjectMapper();
            String content = objectMapper.writeValueAsString(exception.getMessage());
            response.getWriter().write(content);
        }
    }
    

    UserService.java

    //为方便,模拟从数据库查询用户信息
    @Service
    public class UserService implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Set<GrantedAuthority> authorities = new HashSet<>();
    
            authorities.add(new SimpleGrantedAuthority("admin"));
            return new User("fly",passwordEncoder.encode("123"),authorities);
        }
    }
    

    效果:

    访问http://localhost:8080/test,返回{"result":"0","message":"请登录","data":null}

    访问http://localhost:8080/login.html,输入错误用户名或密码,返回{"result":"0","message":"用户名或密码错误","data":null}//具体错误信息可通过覆盖DaoAuthenticationProvider或者在UserDetailService中添加逻辑代码实现。输入正确用户名 密码,返回

    {
        "authorities": [{
            "authority": "admin"
        }],
        "details": {
            "remoteAddress": "0:0:0:0:0:0:0:1",
            "sessionId": "21385DFFE1DC1DBEE43CDB6F8FD00444"
        },
        "authenticated": true,
        "principal": {
            "password": null,
            "username": "fly",
            "authorities": [{
                "authority": "admin"
            }],
            "accountNonExpired": true,
            "accountNonLocked": true,
            "credentialsNonExpired": true,
            "enabled": true
        },
        "credentials": null,
        "name": "fly"
    }
    

    自定义格式在SuccessHandler中实现。

    过程分析

    一张经典的SpringSecurity执行图

    SpringSecurity的执行是依赖一套FilterChain来实现的

    SecurityContextPersistenceFilter是第二个Filter,通过断点方式来看这条Filter Chain

    1571972343156.png

    比较重要的几个Filter:

    SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,用于FilterSecurityInterceptor判断是否已经认证,SecurityContextHolder中存储。
    ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();SecurityContextHolder中的SecurityContext是线程级变量
    
    UsernamePasswordAuthenticationFilter:拦截请求(看其构造函数中配置)。这里采用了模板的方式,具体拦截过程在AbstractAuthenticationProcessingFilter中的doFilter方法。
    
    FilterSecurityInterceptor:根据配置,即那些请求需要permitAll ,哪些请求需要authenticated。
    

    主要的验证逻辑是在UsernamePasswordAuthenticationFilter中实现的

    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#UsernamePasswordAuthenticationFilter(构造函数)

    
        public UsernamePasswordAuthenticationFilter() {
        //这个类继承了一个抽象类,这里的构造函数设置了父类的requiresAuthenticationRequestMatcher属性(在判断是否拦截请求的时候用到)
            super(new AntPathRequestMatcher("/login", "POST"));
        }
    

    org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter

            ...
            //用上面的requiresAuthenticationRequestMatcher进行匹配判断,即拦截路径/login POST请求
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
    
                return;
            }
            ...
            
            try {
            //尝试进行验证 子类实现(关键方法)
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch{
            ...
            }
            ...
            //调用登录成功处理器
            successfulAuthentication(request, response, chain, authResult);
    

    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

        public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String username = obtainUsername(request);
            String password = obtainPassword(request);
    
            if (username == null) {
                username = "";
            }
    
            if (password == null) {
                password = "";
            }
    
            username = username.trim();
    
            //封装用于验证的token
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
    
            // Allow subclasses to set the "details" property sessionID,封装请求的一些信息
            setDetails(request, authRequest);
    
            //得到AuthenticationManager对象(全局唯一)的实现类ProviderManager进行登录验证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    

    ​ org.springframework.security.authentication.ProviderManager#authenticate

    ...
    //通过循环所注册的provider,通过provider的support(token)方法查找能处理该类型token的provider
    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
                    continue;
                }
    }
    try {
                //支持UsernamePasswordAuthenticationToken的是DaoAuthenticationProvider
                    result = provider.authenticate(authentication);
    
                    if (result != null) {
                        copyDetails(authentication, result);
                        break;
                    }
                }
    

    org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate

    preAuthenticationChecks.check(user);
    additionalAuthenticationChecks(user),
    

    org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser

    //利用userDetailService查找用户,security自动配置
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
                if (loadedUser == null) {
                    throw new InternalAuthenticationServiceException(
                            "UserDetailsService returned null, which is an interface contract violation");
                }
                return loadedUser;
    

    org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks

    ...
    //这个方法对密码进行了校验
        String presentedPassword = authentication.getCredentials().toString();
    
            if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");
    
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
    

    整体的流程是这样的,

    一张经典的图

    1571974749801.png

    所以可以通过添加自定义filter provider token可以实现自定义登录流程,

    相关文章

      网友评论

          本文标题:SpringSecurity

          本文链接:https://www.haomeiwen.com/subject/lccevctx.html