Spring Security详解
作者:mmseoamin日期:2023-12-13

第一节 Spring Security 简介

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。

  • 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

    对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

    第二节 Spring Security 核心组件

    Spring Security详解,第1张

     

    1. Authentication 认证

    //这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken
    //我们自定义实现就可以叫MobileCodeAuthenticationToken
    public interface Authentication extends Principal, Serializable {
    ​
        /**
         * 权限列表
         */
        Collection getAuthorities();
    ​
        /**
         * 认证凭据,可能是密码,也可能是验证码,也可能是其他认证凭据
         */
        Object getCredentials();
    ​
        /**
         * 认证请求的详细信息,可能是IP地址,也可能是认证序列号,也可能是null
         */
        Object getDetails();
    ​
        /**
         * 如果通过认证,则返回的是包含(用户名和密码)或者(手机号和验证码)等的对象;如果认证不通过,
         * 则返回的是用户名或者手机号等。
         */
        Object getPrincipal();
    ​
        /**
         * 是否认证通过
         */
        boolean isAuthenticated();
    ​
        /**
         * 修改认证状态
         */
        void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    }

    Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等

    2. UsernamePasswordAuthenticationFilter

    账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。

    3. AuthenticationManager

    public interface AuthenticationManager {
    ​
       /**
        * 尝试认证传递过来的认证信息,如果认证成功,则会修改认证信息的状态。否则,则会抛出异常
        */
       Authentication authenticate(Authentication authentication) throws AuthenticationException;
    ​
    }

    认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。

    4. ProviderManager

    public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
        //省略其他属性
        private List providers = Collections.emptyList();
        //省略其他内容
    }

    认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。

    5. AuthenticationProvider

    public interface AuthenticationProvider {
    ​
        /**
         * 执行认证,并返回认证结果
         */
        Authentication authenticate(Authentication authentication) throws AuthenticationException;
    ​
        /**
         * 支持认证的类型,用于实现自定义认证,比如手机号和短信登录认证需要用户自己来实现
         */
        boolean supports(Class authentication);
    }

    认证提供者,这是一个接口,具体如何认证,就看如何实现该接口。Spring Security 提供了 DaoAuthenticationProvider 实现该接口,这个类就是使用数据库中数据进行认证。

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
        //省略其他属性
        private PasswordEncoder passwordEncoder; //密码加密器,主要用于密码加密
        private UserDetailsService userDetailsService; //用户详细信息服务,主要用于查询认证用户信息
        //省略其他内容
    }

    6. UserDetailsService

    public interface UserDetailsService {
    ​
       /**
        * 根据用户名获取用户详细信息
        */
       UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }

    7. UserDetails

    public interface UserDetails extends Serializable {
    ​
        /**
         * 用户拥有的权限列表
         */
        Collection getAuthorities();
    ​
        /**
         * 密码
         */
        String getPassword();
    ​
        /**
         * 账号
         */
        String getUsername();
    ​
        /**
         * 账号是否过期,过期的账号不能进行认证
         */
        boolean isAccountNonExpired();
    ​
        /**
         * 账号是否被锁定
         */
        boolean isAccountNonLocked();
    ​
        /**
         * 凭据是否过期,过期的凭据不能进行认证
         */
        boolean isCredentialsNonExpired();
    ​
        /**
         * 账号是否被启用,未启用的账号不能进行认证
         */
        boolean isEnabled();
    }

    用户的详细信息,主要用于登录认证。

    8. SecurityContextHolder

    SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

    9. SecurityContext

    SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。

    10. AuthenticationSuccessHandler

    AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。

    11. AuthenticationFailureHandler

    AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。

    12. AccessDecisionManager

    AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。

    13. AccessDeniedHandler

    AccessDeniedHandler 主要用于无权访问时的处理

    第三节 Spring Security 工作流程

    1. DelegatingFilterProxy

    public class DelegatingFilterProxy extends GenericFilterBean {}
    ​
    public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
            EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}

    由上面的类定义可以看出,DelegatingFilterProxy是一个 Filter,同时,也是一个InitializingBean。

    public interface InitializingBean {
    ​
        /**
         * 当Bean的所有属性设置后由包含的BeanFactory调用,而BeanFactory就是用来创建bean的,换言之,就是将Bean纳入Spring IOC容器
         */
        void afterPropertiesSet() throws Exception;
    }

    实现了InitializingBean接口的类,在创建对象并完成属性设置后,会被纳入Spring IOC 容器管理。如果DelegatingFilterProxy在web.xml中配置,那么,在容器启动时就会实例化该Filter,然后完成初始化,随后被纳入 Spring IOC 容器管理。这样就相当于与 Spring 完成整合。

    而 DelegatingFilterProxy 由 spring web 提供,与 Spring Security 无关。那么 DelegatingFilterProxy 到底有什么作用呢?

    其作用是代理真正的Filter实现类

    DelegatingFilterProxy 如何知道其所代理的Filter是哪个呢?

    这是通过其自身的targetBeanName的属性来确定的,通过该名称,DelegatingFilterProxy可以从WebApplicationContext中获取指定的 bean 作为代理对象。该属性可以通过在web.xml 中定义 DelegatingFilterProxy 时通过 init-param 来指定,如果未指定,则将取其在web.xml中声明时定义的名称作为 targetBeanName 的值。

    
    
        
        springSecurityFilterChain
        org.springframework.web.filter.DelegatingFilterProxy
        
            targetBeanName
            springSecurityFilterChain
        
    
    
        springSecurityFilterChain
        /*
    

    2. FilterChainProxy

    使用Spring Security时,DelegatingFilterProxy 代理的就是一个 FilterChainProxy。当我们使用基于Spring Security的NameSpace进行配置时,系统会自动为我们注册一个名为 springSecurityFilterChain 类型为 FilterChainProxy 的 Bean,这也是为什么我们在使用 Spring Security 时需要在 web.xml 中声明一个 name 为 springSecurityFilterChain 的 DelegatingFilterProxy 的 Filter 了。

    FilterChainProxy 有什么作用呢?

    一个 FilterChainProxy 中可以包含有多个 FilterChain,但是某个请求只会对应一个 FilterChain,而一个 FilterChain 中又可以包含有多个 Filter。

    而 Spring Security 底层正是通过一系列的 Filter 来工作的。具体详情如下:

    2.1 WebAsyncManagerIntegrationFilter

    将Security上下文与SpringWeb中用于处理异步请求映射的WebAsyncmanager进行集成

    2.2 SecurityContextPersistenceFilter

    在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后麻将SecurityContextHolder中关于这次请求的信息存储的‘仓储’中,然后将SecurityContextHolder中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的

    2.3 HeaderWriterFilter

    用于将头信息加入响应中

    2.4 CsrfFilter

    用于处理跨站请求伪造

    2.5 LogoutFilter

    用于处理退出登录

    2.6 UsernamePasswordAuthenticationFilter

    [重点]

    用于处理基于表单的登录请求,从表单中获取用户名和密码,默认情况下处理来自/login的请求,从表单中获取用户名和密码, 默认使用表单name值为username和password,这两个值可以通过这个过滤器的usernaemparamter个passwordParameter连个参数的值进行修改

    2.7 DefaultLoginPageGeneratingFilter

    如果没有配置登陆页面,那系统初始化就会配置这个过滤器。并且用于在需要进行登陆时生成一个登陆表单页

    2.8 BasicAuthenticationFilter

    检测和处理http basic认证

    2.9 RequestCacheAwareFilter

    用于处理请求的缓存

    2.10 SecurityContextHolderAwareRequestFilter

    主要包装请求对象request

    2.11 AnonymousAuthenticationFilter

    检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication

    2.12 SessionManagementFilter

    管理session的过滤器

    2.13 ExceptionTranslationFilter

    处理AccessDeniedException和AuthenticationException异常

    2.14 FilterSecurityInterceptor

    可以看作过滤器链的出口

    2.15 RememberMeAuthenticationFilter

    当用户没有登录而直接访问资源时,从cookie中找出用户的信息,如果SpringSecurity能够识别出用户提供remember me cookie ,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认从不开启

    第三节 Spring Security 认证

    Spring Security详解,第2张 

     

    1. 数据库认证

    1.1 配置认证管理器

    package com.qf.authentication.config;
    ​
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    ​
    @EnableWebSecurity //启用security
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ​
        //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    ​
        //认证管理器配置
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //数据库数据认证提供器
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            //设置认证使用的用户详情服务,业就是查询用户信息的服务
            daoAuthenticationProvider.setUserDetailsService();
            //设置密码使用的加密器
            daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
            //设置认证管理构建器使用的认证提供器
            auth.authenticationProvider(daoAuthenticationProvider);
        }
    }

    1.2 创建查询用户信息服务

    package com.qf.authentication.service;
    ​
    import org.springframework.security.core.userdetails.UserDetailsService;
    ​
    //用户业务层,继承了UserDetailsService,方便与security结合
    public interface UserService extends UserDetailsService {
    }
    ​
    ​
    package com.qf.authentication.service.impl;
    ​
    import com.qf.authentication.service.UserService;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    ​
    @Service
    public class UserServiceImpl implements UserService {
    ​
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return null;
        }
    }

    1.4 创建用户实体

    package com.qf.authentication.model;
    ​
    import lombok.Data;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    ​
    import java.util.List;
    ​
    @Data
    public class User implements UserDetails {
    ​
        private String username; //账号
    ​
        private String password; //密码
    ​
        private List authorities; //拥有的权限
    ​
        @Override
        public boolean isAccountNonExpired() { //账号是否未过期
            return true;
        }
    ​
        @Override
        public boolean isAccountNonLocked() { //账号是否未被锁定
            return true;
        }
    ​
        @Override
        public boolean isCredentialsNonExpired() {//凭据是否未过期
            return true;
        }
    ​
        @Override
        public boolean isEnabled() {//账号是否可用
            return true;
        }
    }

    1.5 完善用户信息服务

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        user.setUsername("admin");
        user.setPassword(passwordEncoder.encode("123456"));
        user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER")));
        return user;
    }

    1.6 完善认证管理器配置

    //认证管理器配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库数据认证提供器
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //设置认证使用的用户详情服务,业就是查询用户信息的服务
        daoAuthenticationProvider.setUserDetailsService(userService);
        //设置密码使用的加密器
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        //设置认证管理构建器使用的认证提供器
        auth.authenticationProvider(daoAuthenticationProvider);
    }

    1.7 HTTP认证配置

    package com.qf.authentication.config;
    ​
    import com.qf.authentication.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    ​
    @EnableWebSecurity //启用security
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ​
        @Autowired
        private UserService userService;
    ​
        //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    ​
        //认证管理器配置
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //数据库数据认证提供器
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            //设置认证使用的用户详情服务,业就是查询用户信息的服务
            daoAuthenticationProvider.setUserDetailsService(userService);
            //设置密码使用的加密器
            daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
            //设置认证管理构建器使用的认证提供器
            auth.authenticationProvider(daoAuthenticationProvider);
        }
    ​
        //Http认证配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();//关闭跨站请求模拟
            //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
            http.formLogin().loginPage("/").loginProcessingUrl("/login")
                .successHandler().failureHandler().permitAll();
            http.authorizeRequests().anyRequest().authenticated();
            //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
            http.logout().invalidateHttpSession(true).permitAll();
        }
    }
    ​

    1.8 创建认证处理器

    package com.qf.authentication.handler;
    ​
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    ​
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    ​
    //认证成功的处理器
    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    ​
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.sendRedirect("/main.html");
        }
    }
    ​
    ​
    package com.qf.authentication.handler;
    ​
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    ​
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    ​
    //认证失败的处理器
    @Component
    public class LoginFailureHandler implements AuthenticationFailureHandler {
    ​
    ​
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.sendRedirect("/");
        }
    }

    1.9 完善HTTP认证配置

    //Http认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨站请求模拟
        //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
        http.formLogin().loginPage("/").loginProcessingUrl("/login")
            .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
        http.logout().invalidateHttpSession(true).permitAll();
    }

    1.10 页面创建

    index.html

    
    
    
        
        Security登录
    
    
        
                               

    main.html

    
    
    
        
        Security登录成功
    
    
        认证通过了
    
    

    1.11 启动程序进行测试

    1.12 核心流程梳理

    登录请求被 UsernamePasswordAuthenticationFilter 拦截,该拦截器尝试认证,认证过程中调用 AuthenticationManager进行认证。AuthenticationManager进行认证时,将该认证管理器中的所有认证提供器遍历一遍,遍历过程中,首先检测认证提供器是否支持认证的票据类型,如果支持,则认证提供器开始进行认证。认证提供器认证过程中会调用 UserDetailsService 获取用户信息,然后进行信息比对,如果正确,则返回一个认证通过的票据。所有认证提供器中,只要任意一个认证提供器认证通过,则表示认证成功。

    2. 短信认证

    2.1 创建短信认证过滤器

    package com.qf.authentication.sms;
    ​
    import org.springframework.lang.Nullable;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    ​
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    ​
    //短信认证提供器,模仿UsernamePasswordAuthenticationFilter编写
    public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    ​
        //短信登录使用的URL,请求类型必须时POST
        private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms","POST");
    ​
    ​
        public SmsAuthenticationFilter() {
            super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
        }
    ​
        public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
        }
    ​
        //尝试认证
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException, IOException, ServletException {
            return null;
        }
    ​
    ​
        //获取短信验证码
        @Nullable
        protected String obtainCode(HttpServletRequest request) {
            return request.getParameter("code");
        }
    ​
        //获取手机号码
        @Nullable
        protected String obtainMobile(HttpServletRequest request) {
            return request.getParameter("mobile");
        }
    }

    2.2 创建短信认证票据

    package com.qf.authentication.sms;
    ​
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    import org.springframework.util.Assert;
    ​
    import java.util.Collection;
    ​
    //短信认证的票据
    public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    ​
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    ​
        //认证之前存储手机号码,认证之后存储的是用户信息,也就是一个User对象
        private final Object principal;
    ​
        //验证码
        private  Object credentials;
    ​
        /**
         * 认证之前使用
         * @param principal
         */
        public SmsAuthenticationToken(Object principal, Object credentials){
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            super.setAuthenticated(false);
        }
    ​
        /**
         * 认证之后使用
         * @param principal
         * @param authorities
         */
        public SmsAuthenticationToken(Object principal, Object credentials, Collection authorities) {
            super(authorities);
            this.principal = principal;
            this.credentials = credentials;
            super.setAuthenticated(true);
        }
    ​
        @Override
        public Object getCredentials() {
            return credentials;
        }
    ​
        @Override
        public Object getPrincipal() {
            return principal;
        }
    ​
        //这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证
        @Override
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            Assert.isTrue(!isAuthenticated,
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            super.setAuthenticated(false);
        }
    ​
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
            this.credentials = null;
        }
    }

    2.3 创建短信实体

    package com.qf.authentication.sms;
    ​
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    ​
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class SmsCode {
        //手机号
        private String mobile;
        //验证码
        private String code;
        //过期时间
        private long expire;
    }

    2.4 短信模拟

    package com.qf.authentication.controller;
    ​
    import com.qf.authentication.sms.SmsCode;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    ​
    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.awt.*;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.Random;
    ​
    ​
    @Controller
    public class SmsController {
    ​
        @GetMapping("/code")
        public void imgCode(@RequestParam("mobile")String mobile, HttpSession session, HttpServletResponse response) throws IOException {
            BufferedImage bi = new BufferedImage(200, 40, BufferedImage.TYPE_INT_RGB);
            Graphics graphics = bi.getGraphics();
            graphics.setColor(Color.GRAY);
            graphics.fillRect(0, 0, 200, 40);
            StringBuilder builder = new StringBuilder();
            Random r = new Random();
            for(int i=0; i<6; i++){
                int num = r.nextInt(10);
                builder.append(num);
                graphics.setColor(Color.red);
                graphics.drawString(Integer.toString(num), i*10 + 20, 15);
            }
            //创建短信实体
            SmsCode code = new SmsCode(mobile, builder.toString(), System.currentTimeMillis() + 5 * 60 * 1000);
            //将短信实体放入session中
            session.setAttribute("smsCode", code);
            graphics.dispose();
            ImageIO.write(bi, "jpg", response.getOutputStream());
        }
    }

    2.5 完善短信认证过滤器

    //尝试认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if(!request.getMethod().equalsIgnoreCase("POST")){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String code = obtainCode(request);//获取短信验证码
        //从session中获取发送的短信验证码信息
        SmsCode smsCode = (SmsCode) request.getSession().getAttribute("smsCode");
        if(code == null || smsCode == null){
            throw new AuthenticationServiceException("SMS code cannot be null");
        }
        String mobile = obtainMobile(request);//获取手机号
        long currentTime = System.currentTimeMillis();//获取系统当前时间
        if(smsCode.getExpire() < currentTime || !mobile.equals(smsCode.getMobile())){//如果系统当前时间比验证码过期时间还要大,说明验证码过期,手机号码与验证码不匹配
            throw new AuthenticationServiceException("SMS code is invalid:" + smsCode.getCode());
        } else if(!code.equals(smsCode.getCode())){
            throw new AuthenticationServiceException("SMS code error");
        }
        SmsAuthenticationToken token = new SmsAuthenticationToken(mobile, code);//创建SMS token
        this.setDetails(request, token);
        return this.getAuthenticationManager().authenticate(token);//调用认证管理器认证token
    }
    ​
    //将请求信息放入token中
    protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    2.6 创建短信认证提供器

    package com.qf.authentication.sms;
    ​
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    ​
    public class SmsAuthenticationProvider implements AuthenticationProvider {
    ​
        //获取认证用户的信息的服务接口
        private UserDetailsService userDetailsService;
    ​
        /**
         * 这个方法就是认证,如果没有抛出认证异常,说明认证成功
         * @param authentication  未进行认证的信息,里面就是包含了一个mobile信息和请求的信息
         * @return 返回一个认证完成的信息
         * @throws AuthenticationException
         */
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String mobile = (String) authentication.getPrincipal();
            UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //通过手机号码获取用户信息
            SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
            authenticationToken.setDetails(authentication.getDetails());
            return authenticationToken;
        }
    ​
        @Override
        public boolean supports(Class authentication) {
            // 判断 authentication 是不是 SmsCodeAuthenticationToken 类型或者其子类或者其子接口
            return SmsAuthenticationToken.class.isAssignableFrom(authentication);
        }
    ​
        public UserDetailsService getUserDetailsService() {
            return userDetailsService;
        }
    ​
        public void setUserDetailsService(UserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    }

    2.7 配置短信认证

    package com.qf.authentication.config;
    ​
    import com.qf.authentication.handler.LoginFailureHandler;
    import com.qf.authentication.handler.LoginSuccessHandler;
    import com.qf.authentication.service.UserService;
    import com.qf.authentication.sms.SmsAuthenticationFilter;
    import com.qf.authentication.sms.SmsAuthenticationProvider;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    ​
    @EnableWebSecurity //启用security
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ​
        @Autowired
        private UserService userService;
    ​
        @Autowired
        private LoginSuccessHandler loginSuccessHandler;
    ​
        @Autowired
        private LoginFailureHandler loginFailureHandler;
    ​
        @Autowired
        @Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器
        private AuthenticationManager authenticationManager;
    ​
        //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    ​
        //认证管理器配置
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //创建短信认证提供器
            SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
            smsAuthenticationProvider.setUserDetailsService(userService);
            //将认证提供器添加到认证管理器中
            auth.authenticationProvider(smsAuthenticationProvider);
    ​
            //数据库数据认证提供器
            DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
            //设置认证使用的用户详情服务,业就是查询用户信息的服务
            daoAuthenticationProvider.setUserDetailsService(userService);
            //设置密码使用的加密器
            daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
            //设置认证管理构建器使用的认证提供器
            auth.authenticationProvider(daoAuthenticationProvider);
        }
    ​
        //Http认证配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();//关闭跨站请求模拟
            //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
            http.formLogin().loginPage("/").loginProcessingUrl("/login")
                    .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
            //设置获取验证码的请求放行
            http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
                    //其他请求需要认证
                    .anyRequest().authenticated();
            //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
            http.logout().invalidateHttpSession(true).permitAll();
            //将短信认证过滤器添加账号密码过滤器的前面
            http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        }
    ​
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    ​
        @Bean
        public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {
            SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
            //设置短信认证过滤器使用的认证管理器
            smsAuthenticationFilter.setAuthenticationManager(authenticationManager);
            //设置登录成功的处理器
            smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
            //设置登录失败的处理器
            smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);
            return smsAuthenticationFilter;
        }
    }

    2.8 修改登录页面

    index.html

    
    
    
        
        Security登录
    
    
        
                               
    ​    
                                       

    2.9 启动程序进行测试

    第四节 Spring Security 授权

    Spring Security详解,第3张 

     

    1. 启用注解授权

    @EnableWebSecurity //启用security
    //prePostEnabled = true启用@PreAuthorize()
    //securedEnabled = true启用@Secured()
    //jsr250Enabled = true启用@RolesAllowed、@PermitAll、@DenyAll 但该注解需要jar包支撑
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {}

    JSR 250 依赖包

    
    
        javax.annotation
        jsr250-api
        1.0
    

    2. HTTP授权配置

    //Http认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨站请求模拟
        //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
        http.formLogin().loginPage("/").loginProcessingUrl("/login")
            .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
        //设置获取验证码的请求放行
        http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
             //授权请求表示任意请求都需要认证才能够访问
            .anyRequest().authenticated();
        //设置异常处理使用访问拒绝处理器
        http.exceptionHandling().accessDeniedHandler();
        //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
        http.logout().invalidateHttpSession(true).permitAll();
        //将短信认证过滤器添加账号密码过滤器的前面
        http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    3. 创建拒绝请求处理器

    package com.qf.authentication.handler;
    ​
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    ​
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    ​
    //HTTP请求被拒绝的处理器
    @Component
    public class RequestDeniedHandler implements AccessDeniedHandler {
    ​
        //这里就是拒绝处理的具体步骤实现
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setContentType("text/html;charset=utf-8");
            //返回拒绝处理的信息
            response.getWriter().print(accessDeniedException.getMessage());
        }
    }

    5. 完善HTTP授权配置

    @Autowired
    private RequestDecisionManager decisionManager;
    ​
    @Autowired
    private RequestDeniedHandler deniedHandler;
    ​
    //Http认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨站请求模拟
        //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
        http.formLogin().loginPage("/").loginProcessingUrl("/login")
            .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
        //设置获取验证码的请求放行
        http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
             //授权请求表示任意请求都需要认证才能够访问
            .anyRequest().authenticated();
        //设置异常处理使用访问拒绝处理器
        http.exceptionHandling().accessDeniedHandler(deniedHandler);
        //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
        http.logout().invalidateHttpSession(true).permitAll();
        //将短信认证过滤器添加账号密码过滤器的前面
        http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    6. 创建测试请求

    package com.qf.authentication.controller;
    ​
    import org.springframework.security.access.annotation.Secured;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    ​
    import javax.annotation.security.RolesAllowed;
    ​
    @RestController
    public class TestController {
    ​
        @GetMapping("/test1")
        @PreAuthorize("hasRole('ROLE_ADMIN')")
        public String test1(){
            return "test1";
        }
    ​
        @GetMapping("/test2")
        @PreAuthorize("hasRole('ROLE_TEST')")
        public String test2(){
            return "test2";
        }
    ​
        @GetMapping("/test3")
        @Secured("ROLE_ADMIN")
        public String test3(){
            return "test3";
        }
    ​
        @GetMapping("/test4")
        @Secured("ROLE_TEST")
        public String test4(){
            return "test4";
        }
    ​
        @GetMapping("/test5")
        @RolesAllowed("ROLE_ADMIN")
        public String test5(){
            return "test5";
        }
    ​
        @GetMapping("/test6")
        @RolesAllowed("ROLE_TEST")
        public String test6(){
            return "test6";
        }
    }

    7. 启动程序进行测试

    第五节 Security 与 AJAX 对接

    这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是ajax请求,那么问题即将得到解决。

    package com.qf.security.util;
    ​
    import javax.servlet.http.HttpServletRequest;
    ​
    /**
     * @Author: wu
     * @Description:
     * @Date: 2021-11-09
     */
    //针对请求相关的操作工具类
    public class RequestUtil {
    ​
        private RequestUtil(){}
    ​
        /**
         * 验证请求是否是AJAX请求 这种验证对于jQuery发送的AJAX没有任何问题
         * 但是,对于 axios发送的AJAX可能存在没有X-Requested-With这个头信
         * 息的
         * @param request
         * @return
         */
        public static boolean isAjaxRequest(HttpServletRequest request){
            String header = request.getHeader("X-Requested-With");
            return "XMLHttpRequest".equalsIgnoreCase(header);
        }
    }

    登录的时候传递的参数在过滤器中获取不到,需要注意:在传递参数的时候要使用get方法传递参数的方式对参数进行拼接,然后赋值给data

    $.ajax({
        type: 'post',
        url: 'login',
        data: "username="+ $("#username").val() + "&password=" + $("#password").val(),
        success: function (resp) {
            console.log(resp);
        }
    });