美文网首页
Spring Security 开发基于表单的认证

Spring Security 开发基于表单的认证

作者: 寂静的春天1988 | 来源:发表于2020-07-24 14:57 被阅读0次

    核心功能:1、认证 2、授权 3、攻击防护

    #是否开启security
    security.basic.enabled=true
    

    之前我们把这里设为false,关闭了security,现在打开。看看不做任何配置security做了什么。


    image.png

    可以发现security做了一个弹窗登录的验证,来保护所有的接口服务。


    image.png
    项目启动的时候可以看到它打印了密码。账户是user
    登录之后所有接口就可以访问了。

    配置表单登录

    将配置写在fuiou-security-browser项目下,这样其他项目引入该项目就可以了,demo项目只是一个演示项目

    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .and()
                .authorizeRequests()//请求授权
                .anyRequest()       //任何请求
                .authenticated();//身份认证
        }
        
    }
    

    上面的意思就是任何请求都要进行表单登录的身份验证,密码和账号依旧和弹窗登录一样。

    Spring Security基本原理

    image.png

    解析:一个请求过来,图中绿色的部分是一个个身份验证的过滤器,它们会根据请求的信息去匹配是否通过验证,然后到达filterSecurityInterceptor拦截器。它是整个过滤器链的最后一环,根据前面身份验证过滤器链中是否通过验证,如果没有通过验证,它会抛出异常,被ExceptionTranslationFilter捕获到,根据异常将用户引导到不同的页面。

    图中绿色的部分我们可以通过配置,配置它是否生效,其他的部分是不能更改的。

    自定义用户认证逻辑

    这里我们不去数据库查询,只是模拟一下。

    1、处理用户信息获取的逻辑

    @Slf4j
    @Component
    public class MyUserDetailsService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            log.info("登录用户名==》"+username);
            // 根据用户名去数据库中查询用户信息
            return new User(username, "12345", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
    
    }
    

    new User()对象,是Security的提供的一个对象,前两个参数是用户名和密码,第三个是该用户所拥有的权限(权限后面再讲,先随便传一个)

    2、处理用户校验的逻辑
    UserDetails中提供了四个方法,来返回额外的验证。

    isAccountNonExpired();账号没有过期
    isAccountNonLocked();账号没有被冻结
    isCredentialsNonExpired();密码没有过期
    isEnabled();账号可用

    public class MyUserDetailsService implements UserDetailsService {
        
        
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            log.info("登录用户名==》"+username);
            // 根据用户名去数据库中查询用户信息
            
            // 根据查找的用户信息,判断用户是否被冻结
            
            return new User(username, "12345",true,true,true,false,AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
    
    }
    

    这里将账号未被锁定的设为false


    image.png

    注意:这里使用的是security提供的user类,在实际开发中可以用自己的user类来实现UserDetailsService接口,返回该user类即可。

    3、处理密码加密解密

    org.springframework.security.crypto.password包下提供了一个PasswordEncoder接口。
    String encode(CharSequence rawPassword);该方法用于密码加密,在插入数据库之前需要使用该方法进行加密后,然后插入到数据库中。
    boolean matches(CharSequence rawPassword, String encodedPassword);验证从存储获取的编码密码是否与提交的原始密码匹配

    BrowserSecurityConfig

        @Bean
        public PasswordEncoder passwordEncoder() {
            // 如果项目内已经有自己的加密解密逻辑,返回自己的PasswordEncoder
            return new BCryptPasswordEncoder();
        }
    
    @Slf4j
    @Component
    public class MyUserDetailsService implements UserDetailsService {
        
        @Autowired
        private PasswordEncoder passwordEncoder;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            log.info("登录用户名==》"+username);
            // 根据用户名去数据库中查询用户信息
            
            // 根据查找的用户信息,判断用户是否被冻结
            
            //模拟一下数据库中加密的密码
            String password=passwordEncoder.encode("12345");
            
            return new User(username, password,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
    
    }
    

    个性化用户认证流程

    1、自定义登录页面

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .loginPage("/fuiou-login.html")//登录页面
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/fuiou-login.html")
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated();//身份认证
        }
        
    
    image.png
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>标准登录页</title>
    </head>
    <body>
        标准登录
        <form action="/authentication/form" method="post">
            <table>
                <tr>
                    <td>用户名</td>
                    <td><input name="username" /></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="password" name="password" /></td>
                </tr>
                <tr>
                    <td colspan="2">
                        <button type="submit">登录</button>
                    </td>
                </tr>
            </table>
            
        </form>
    </body>
    </html>
    

    登录一下:发现报了一个403的错误,因为security开启了CSRF防护。

    暂时先关闭csrf防护

            http.formLogin()
                .loginPage("/fuiou-login.html")//登录页面
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/fuiou-login.html")
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
    

    需求:如果是请求html就返回登录页面,如果请求接口则返回状态码和错误信息。引入browser项目可以使用自己的登录页


    image.png
    @RestController
    @Slf4j
    public class BrowserSecurityController {
        // security判断需要验证跳转到controller之前,将当前请求缓存到了HttpSessionRequestCache
        private RequestCache requestCache=new HttpSessionRequestCache();
        
        private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
        
        @Autowired
        private SecurityProperties securityProperties;
        /**
         * 当需要身份认证时跳转到这里
         * 
         * @param request
         * @param response
         * @return
         * @throws IOException 
         */
        @RequestMapping("/authentication/requrie")
        @ResponseStatus(code=HttpStatus.UNAUTHORIZED)
        public SimpleResponse requrieAuthentication(HttpServletRequest request,HttpServletResponse response) throws IOException {
            // 取得引发跳转的请求
            SavedRequest savedRequest=requestCache.getRequest(request, response);
            if(savedRequest!=null) {
                String targetUrl=savedRequest.getRedirectUrl();
                log.info("引发跳转的请求是===>"+targetUrl);
                if(StringUtils.endsWith(targetUrl, ".html")) {
                    redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
                }
            }
            return new SimpleResponse("访问的服务需要身份认证,引导用户到登录页");
        }
    }
    

    fuiou-security-core项目下

    @Configuration
    @EnableConfigurationProperties(SecurityProperties.class)
    public class SecurityCoreConfig {
    
    }
    
    @ConfigurationProperties(prefix = "fuiou.security")
    @Data
    public class SecurityProperties {
        private BrowserProperties browser = new BrowserProperties();
    }
    
    @Data
    public class BrowserProperties {
        private String loginPage = "/fuiou-login.html";
    }
    

    fuiou-security-demo项目下:如果想使用自己的登录页那么在application.properties配置号登录页的地址就好了
    application.properties

    fuiou.security.browser.loginPage=/demo-sigin.html
    

    BrowserSecurityConfig 中取消对登录页地址的验证。

    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
        
        
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 如果项目内已经有自己的加密解密逻辑,返回自己的PasswordEncoder
            return new BCryptPasswordEncoder();
        }
        
        @Autowired
        private SecurityProperties securityProperties;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",securityProperties.getBrowser().getLoginPage())
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
        
    }
    
    

    2、自定义登录成功处理
    security默认处理,登录成功后,跳转到原先的请求。

    需求:现在项目很多都是前后端分离的方式进行,访问登录接口,可能是通过ajax异步的方式,这里直接跳转页面的,是不符合Ajax的需求的。

    自定义处理:

    @Component("fuiouAuthenticationSuccessHandler")
    @Slf4j
    public class FuiouAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        
        @Autowired
        private ObjectMapper objectMapper;
        
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            //authentication 封装了认证信息
            log.info("登录成功");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }
    
    }
    

    使用自定义的登录成功处理器fuiouAuthenticationSuccessHandler

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",securityProperties.getBrowser().getLoginPage())
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    3、自定义登录失败处理

    @Component("fuiouAuthenticationFailureHandler")
    @Slf4j
    public class FuiouAuthenticationFailureHandler implements AuthenticationFailureHandler {
        
        @Autowired
        private ObjectMapper objectMapper;
        
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException exception) throws IOException, ServletException {
            // exception 失败的异常信息
            log.info("登录失败");
            // 设置500状态码
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception));
        }
    
    }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",securityProperties.getBrowser().getLoginPage())
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    需求:上面虽然实现了返回json数据的功能,但是由于browser项目是一个通用的项目,可能引入browser的,反而登录成功或者失败后跳转页面更符合需要。

    public enum LoginType {
        REDIRECT,
        
        JSON
    }
    
    @Data
    public class BrowserProperties {
        private String loginPage = "/fuiou-login.html";
        
        private LoginType loginType=LoginType.JSON;
    }
    
    @Component("fuiouAuthenticationFailureHandler")
    @Slf4j
    public class FuiouAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        
        @Autowired
        private ObjectMapper objectMapper;
        
        @Autowired
        private SecurityProperties securityProperties;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException exception) throws IOException, ServletException {
            if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
                // exception 失败的异常信息
                log.info("登录失败");
                // 设置500状态码
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(exception));
            }else {
                super.onAuthenticationFailure(request, response, exception);
            }
        }
    
    }
    
    @Component("fuiouAuthenticationSuccessHandler")
    @Slf4j
    public class FuiouAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
        
        @Autowired
        private ObjectMapper objectMapper;
        
        @Autowired
        private SecurityProperties securityProperties;
        
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
                //authentication 封装了认证信息
                log.info("登录成功");
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(authentication));
            }else {
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    
    }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",securityProperties.getBrowser().getLoginPage())
                .permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    图形验证码功能

    思路:
    1、根据随机数生成图片
    2、将随机数存储到session中
    3、将图片响应到接口

    @NoArgsConstructor
    @Data
    public class ImageCode {
        private BufferedImage image;
        
        private String code;
        
        private LocalDateTime expireTime;
        
        
        public ImageCode(BufferedImage image,String code,int expireSeconds) {
            this.image=image;
            this.code=code;
            this.expireTime=LocalDateTime.now().plusSeconds(expireSeconds);
        }
    
    
        public boolean isExpired() {
            return LocalDateTime.now().isAfter(expireTime);
        }
    }
    
    @RestController
    public class ValidateCodeController {
        
        public static final String SESSION_KEY="SESSION_KEY_IAMGE_CODE";
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        private Random random = new Random();
        
        String randString = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
        
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
            ImageCode imageCode=createImageCode(request);
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        }
        
        
        
        private ImageCode createImageCode(HttpServletRequest request) {
            
            int width=95;
            
            int height=35;
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
     
            // 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
            Graphics g = image.getGraphics();
            // 图片大小
            g.fillRect(0, 0, width, height);
            // 字体大小
            g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18));
            // 字体颜色
            g.setColor(getRandColor(110, 133));
            
            // 干扰线数量
            int lineSize = 20;
            // 绘制干扰线
            for (int i = 0; i <= lineSize; i++) {
                int x = random.nextInt(width);
                int y = random.nextInt(height);
                int xl = random.nextInt(width);
                int yl = random.nextInt(height);
                g.drawLine(x, y, x + xl, y + yl);
            }
            // 绘制随机字符
            String randomString = "";
            for (int i = 1; i <= 4; i++) {
                randomString = drowString(g, randomString, i);
            }
            g.dispose();
    
            return new ImageCode(image, randomString, 60);
        }
        /**
         * 获得颜色
         */
        private Color getRandColor(int fc, int bc) {
            if (fc > 255) {
                fc = 255;
            }
            if (bc > 255) {
                bc = 255;
            }
            int r = fc + random.nextInt(bc - fc - 16);
            int g = fc + random.nextInt(bc - fc - 14);
            int b = fc + random.nextInt(bc - fc - 18);
            return new Color(r, g, b);
        }
        /**
         * 绘制字符串
         */
        private String drowString(Graphics g, String randomString, int i) {
            g.setFont(getFont());
            g.setColor(new Color(random.nextInt(101), random.nextInt(111), random.nextInt(121)));
            String rand = String.valueOf(getRandomString(random.nextInt(randString.length())));
            randomString += rand;
            g.translate(random.nextInt(3), random.nextInt(4));
            g.drawString(rand, 16 * i, 25);
            return randomString;
        }
        /**
         * 获得字体
         */
        private Font getFont() {
            return new Font("Fixedsys", Font.CENTER_BASELINE, 30);
        }
        /**
         * 获取随机的字符
         */
        public String getRandomString(int num) {
            return String.valueOf(randString.charAt(num));
        }
    }
    
    
    public class ValidateCodeException extends AuthenticationException {
    
        /**
         * 
         */
        private static final long serialVersionUID = -6765936919791004720L;
    
        public ValidateCodeException(String msg) {
            super(msg);
        }
    
    }
    
    public class ValidateCodeFilter extends OncePerRequestFilter {
        
        @Setter
        private AuthenticationFailureHandler authenticationFailureHandler;
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            if(StringUtils.equals("/authentication/form", request.getRequestURI())
                    &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
                try {
                    validate(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
                
            }
            filterChain.doFilter(request, response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
             // 从session中获取图片验证码
            ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
            // 从请求中获取用户填写的验证码
            String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
            if (StringUtils.isBlank(imageCodeInRequest)) {
                throw new ValidateCodeException("验证码不能为空");
            }
            if (null == imageCodeInSession) {
                throw new ValidateCodeException("验证码不存在");
            }
            if (imageCodeInSession.isExpired()) {
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已过期");
            }
            if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) {
                throw new ValidateCodeException("验证码不匹配");
            }
            // 验证成功,删除session中的验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            
        }
        
        
    }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter filter=new ValidateCodeFilter();
            filter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
            
            http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",
                            securityProperties.getBrowser().getLoginPage(),
                            "/code/image").permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    分析:通过增加一个自定义的图片验证码的filter,加在表单登录的filter前,如果验证验证码失败直接抛出异常。

    重构图形验证码接口

    image.png

    1、基本参数配置化

    @Data
    public class ImageCodeProperties {
    
        private int width = 67;
    
        private int heigt = 23;
    
        private int length = 4;
    
        private int expireIn = 60;
    
    }
    
    @Data
    public class VaildateCodeProperties {
        private ImageCodeProperties image=new ImageCodeProperties();
    }
    
    @ConfigurationProperties(prefix = "fuiou.security")
    @Data
    public class SecurityProperties {
        private BrowserProperties browser = new BrowserProperties();
    
        private VaildateCodeProperties code = new VaildateCodeProperties();
    }
    
    private ImageCode createImageCode(HttpServletRequest request) {
            
            int width=ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
            int height=ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeigt());
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
            // 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
            Graphics g = image.getGraphics();
            // 图片大小
            g.fillRect(0, 0, width, height);
            // 字体大小
            g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18));
            // 字体颜色
            g.setColor(getRandColor(110, 133));
            
            // 干扰线数量
            int lineSize = 20;
            // 绘制干扰线
            for (int i = 0; i <= lineSize; i++) {
                int x = random.nextInt(width);
                int y = random.nextInt(height);
                int xl = random.nextInt(width);
                int yl = random.nextInt(height);
                g.drawLine(x, y, x + xl, y + yl);
            }
            // 绘制随机字符
            String randomString = "";
            for (int i = 1; i <= securityProperties.getCode().getImage().getLength(); i++) {
                randomString = drowString(g, randomString, i);
            }
            g.dispose();
    
            return new ImageCode(image, randomString, securityProperties.getCode().getImage().getExpireIn());
        }
    

    分析:配置先从请求当中取,取不到在从securityProperties中取,配置了默认属性,可以从application.properties覆盖。

    2、验证码拦截接口可配

    @Data
    public class ImageCodeProperties {
        
        private int width=150;
        
        private int height=30;
        
        private int length=4;
        
        private int expireIn=60;
        
        private String urls;
    }
    

    application.properties

    fuiou.security.code.image.urls=/hello
    
    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean{
        
        @Setter
        private AuthenticationFailureHandler authenticationFailureHandler;
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        private Set<String> urls=new HashSet<String>();
        
        @Setter
        private SecurityProperties securityProperties;
        
        private AntPathMatcher pathMatcher=new AntPathMatcher();
        
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            String[] configUrls=StringUtils.split(securityProperties.getCode().getImage().getUrls());
            for (String url : configUrls) {
                urls.add(url);
            }
            urls.add("/authentication/form");
        }
        
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            boolean action=false;
            for (String url : urls) {
                if(pathMatcher.match(url, request.getRequestURI())) {
                    action=true;
                    break;
                }
            }
            if(action) {
                try {
                    validate(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
                
            }
            filterChain.doFilter(request, response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
             // 从session中获取图片验证码
            ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
            // 从请求中获取用户填写的验证码
            String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
            if (StringUtils.isBlank(imageCodeInRequest)) {
                throw new ValidateCodeException("验证码不能为空");
            }
            if (null == imageCodeInSession) {
                throw new ValidateCodeException("验证码不存在");
            }
            if (imageCodeInSession.isExpired()) {
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已过期");
            }
            if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) {
                throw new ValidateCodeException("验证码不匹配");
            }
            // 验证成功,删除session中的验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
        }
    }
    
    @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter filter=new ValidateCodeFilter();
            filter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
            filter.setSecurityProperties(securityProperties);
            filter.afterPropertiesSet();
            
            http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",
                            securityProperties.getBrowser().getLoginPage(),
                            "/code/image").permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    解析:思路还是通过properties来配置

    3、验证码生成逻辑可配置化

    public interface ValidateCodeGenerator {
        
        
        public ImageCode generate(HttpServletRequest request);
        
    }
    
    public class ImageCodeGenerator implements ValidateCodeGenerator {
        
        private Random random = new Random();
        
        String randString = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
        
        @Setter
        private SecurityProperties securityProperties;
        
        @Override
        public ImageCode generate(HttpServletRequest request) {
            
            return createImageCode(request);
        }
        private ImageCode createImageCode(HttpServletRequest request) {
            
            int width=ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
            int height=ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
     
            // 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
            Graphics g = image.getGraphics();
            // 图片大小
            g.fillRect(0, 0, width, height);
            // 字体大小
            g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18));
            // 字体颜色
            g.setColor(getRandColor(110, 133));
            
            // 干扰线数量
            int lineSize = 20;
            // 绘制干扰线
            for (int i = 0; i <= lineSize; i++) {
                int x = random.nextInt(width);
                int y = random.nextInt(height);
                int xl = random.nextInt(width);
                int yl = random.nextInt(height);
                g.drawLine(x, y, x + xl, y + yl);
            }
            // 绘制随机字符
            String randomString = "";
            for (int i = 1; i <= securityProperties.getCode().getImage().getLength(); i++) {
                randomString = drowString(g, randomString, i);
            }
            g.dispose();
    
            return new ImageCode(image, randomString, securityProperties.getCode().getImage().getExpireIn());
        }
        /**
         * 获得颜色
         */
        private Color getRandColor(int fc, int bc) {
            if (fc > 255) {
                fc = 255;
            }
            if (bc > 255) {
                bc = 255;
            }
            int r = fc + random.nextInt(bc - fc - 16);
            int g = fc + random.nextInt(bc - fc - 14);
            int b = fc + random.nextInt(bc - fc - 18);
            return new Color(r, g, b);
        }
        /**
         * 绘制字符串
         */
        private String drowString(Graphics g, String randomString, int i) {
            g.setFont(getFont());
            g.setColor(new Color(random.nextInt(101), random.nextInt(111), random.nextInt(121)));
            String rand = String.valueOf(getRandomString(random.nextInt(randString.length())));
            randomString += rand;
            g.translate(random.nextInt(3), random.nextInt(4));
            g.drawString(rand, 16 * i, 25);
            return randomString;
        }
        /**
         * 获得字体
         */
        private Font getFont() {
            return new Font("Fixedsys", Font.CENTER_BASELINE, 30);
        }
        /**
         * 获取随机的字符
         */
        public String getRandomString(int num) {
            return String.valueOf(randString.charAt(num));
        }
    }
    
    @Configuration
    public class ValidateCodeBeanConfig {
        @Autowired
        private SecurityProperties securityProperties;
        
        @Bean
        @ConditionalOnMissingBean(name = "imageCodeGenerator")
        public ValidateCodeGenerator imageCodeGenerator() {
            ImageCodeGenerator imageCodeGenerator=new ImageCodeGenerator();
            imageCodeGenerator.setSecurityProperties(securityProperties);
            return imageCodeGenerator;
        }
    }
    
    @RestController
    public class ValidateCodeController {
        
        public static final String SESSION_KEY="SESSION_KEY_IAMGE_CODE";
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        @Autowired
        private ValidateCodeGenerator imageCodeGenerator;
        
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
            ImageCode imageCode=imageCodeGenerator.generate(request);
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        }
    }
    

    思路:将创建验证码逻辑的代码封装,然后通过@Bean和@ConditionalOnMissingBean实现了,可替换。

    实现记住我的功能

    记住我基本原理.png
    @Autowired
        private DataSource dataSource;
        
        @Autowired
        private UserDetailsService myUserDetailsService;
        
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            tokenRepository.setCreateTableOnStartup(false);
            return tokenRepository;
        }
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter filter=new ValidateCodeFilter();
            filter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
            filter.setSecurityProperties(securityProperties);
            filter.afterPropertiesSet();
            
            http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                .userDetailsService(myUserDetailsService)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",
                            securityProperties.getBrowser().getLoginPage(),
                            "/code/image").permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable();//关闭csrf防护
        }
    

    短信验证码登录

    public class SmsCodeGenerator implements ValidateCodeGenerator {
        
        
        @Setter
        private SecurityProperties securityProperties;
        
        @Override
        public ValidateCode generate(HttpServletRequest request) {
            String code=RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
            return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
        }
    }
    
    public interface SmsCodeSender {
        
        public void send(String mobile,String code);
    }
    
    public class DefaultSmsCodeSender implements SmsCodeSender {
    
        @Override
        public void send(String mobile, String code) {
            System.out.println("mobile===》"+mobile+"===code===>"+code);
    
        }
    }
    
        @Bean
        @ConditionalOnMissingBean(name = "smsCodeGenerator")
        public ValidateCodeGenerator smsCodeGenerator() {
            SmsCodeGenerator smsCodeGenerator=new SmsCodeGenerator();
            smsCodeGenerator.setSecurityProperties(securityProperties);
            return smsCodeGenerator;
        }
        @Bean
        @ConditionalOnMissingBean(name = "smsCodeSender")
        public SmsCodeSender smsCodeSender() {
            SmsCodeSender smsCodeSender=new DefaultSmsCodeSender();
            return smsCodeSender;
        }
    
        @GetMapping("/code/sms")
        public void createSmsCode(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletRequestBindingException {
            ValidateCode validateCode=smsCodeGenerator.generate(request);
            sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, validateCode);
            // 模拟发送到用户
            String mobile=ServletRequestUtils.getRequiredStringParameter(request, "mobile");
            smsCodeSender.send(mobile, validateCode.getCode());
        }
    

    重构验证码代码

    可以发现:图形验证码和短信验证码的发送步骤非常相似
    1、生成验证码
    2、存储到session中
    3、发送到前端/调用短信发送服务发送验证码

    当整个操作相似,其中一部分不同时,可以考虑模板方法。


    重构思路.png
    public interface ValidateCodeProcessor {
        
        public static final String SESSION_KEY="SESSION_KEY_IAMGE_CODE";
        
        public void create(ServletWebRequest request) throws Exception;
    }
    
    public abstract class AbstractVaildateCodeProcessor implements ValidateCodeProcessor{
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        @Autowired
        private Map<String,ValidateCodeGenerator> validateCodeGeneratorMap; 
        
        
        @Override
        public void create(ServletWebRequest request) throws Exception {
            request.getRequest().getRequestURI();
            //创建验证码
            ValidateCode validateCode=this.generate(request);
            //存储到session中
            this.save(request, validateCode.getCode());
            //发送验证码
            this.Send(request, validateCode);
        };
        
        public ValidateCode generate(ServletWebRequest request) {
            String type=this.getProcessorType(request);
            ValidateCodeGenerator validateCodeGenerator=validateCodeGeneratorMap.get(type+"CodeGenerator");
            return validateCodeGenerator.generate(request.getRequest());
        }
        
        public void save(ServletWebRequest request,String validateCode) {
            sessionStrategy.setAttribute(request, SESSION_KEY, validateCode);
        }
        public String getProcessorType(ServletWebRequest request) {
            return StringUtils.substringAfter(request.getRequest().getRequestURI(), "/code/");
        }
        public abstract void Send(ServletWebRequest request,ValidateCode validateCode) throws Exception;
    }
    
    @Component("imageCodeProcessor")
    public class ImageCodeProcessor extends AbstractVaildateCodeProcessor {
    
        @Override
        public void Send(ServletWebRequest request, ValidateCode validateCode) throws IOException {
            ImageCode imageCode=(ImageCode)validateCode;
            ImageIO.write(imageCode.getImage(), "JPEG", request.getResponse().getOutputStream());
        }
    }
    
    @Component("smsCodeProcessor")
    public class SmsCodeProcessor extends AbstractVaildateCodeProcessor {
        
        @Autowired
        private SmsCodeSender smsCodeSender;
        
        @Override
        public void Send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
            String mobile=ServletRequestUtils.getRequiredStringParameter(request.getRequest(), "mobile");
            smsCodeSender.send(mobile, validateCode.getCode());
        }
    }
    
    @RestController
    public class ValidateCodeController {
        
        @Autowired
        private Map<String, ValidateCodeProcessor> processorMap;
        
        @GetMapping("/code/{type}")
        public void createSmsCode(HttpServletRequest request,HttpServletResponse response,@PathVariable String type) throws Exception {
            processorMap.get(type+"CodeProcessor").create(new ServletWebRequest(request, response));
        }
        
    }
    

    这里感叹一下,老师代码确实写得很巧妙。

    短信验证

    @Data
    public class SmsCodeProperties {
        
        private int length=6;
        
        private int expireIn=60;
        
        private String urls="";
        
        
    }
    
    @Data
    public class VaildateCodeProperties {
        private ImageCodeProperties image=new ImageCodeProperties();
        private SmsCodeProperties sms=new SmsCodeProperties();
    }
    
    
    public class SmsCodeGenerator implements ValidateCodeGenerator {
        
        
        @Setter
        private SecurityProperties securityProperties;
        
        @Override
        public ValidateCode generate(HttpServletRequest request) {
            String code=RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
            return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
        }
    }
    
    @Component("smsCodeProcessor")
    public class SmsCodeProcessor extends AbstractVaildateCodeProcessor {
        
        @Autowired
        private SmsCodeSender smsCodeSender;
        
        @Override
        public void Send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
            String mobile=ServletRequestUtils.getRequiredStringParameter(request.getRequest(), "mobile");
            smsCodeSender.send(mobile, validateCode.getCode());
        }
    }
    
    public interface SmsCodeSender {
        
        public void send(String mobile,String code);
    }
    
    
    public class DefaultSmsCodeSender implements SmsCodeSender {
    
        @Override
        public void send(String mobile, String code) {
            System.out.println("mobile===》"+mobile+"===code===>"+code);
    
        }
    
    }
    
    @Configuration
    public class ValidateCodeBeanConfig {
        @Autowired
        private SecurityProperties securityProperties;
        
        @Bean
        @ConditionalOnMissingBean(name = "imageCodeGenerator")
        public ValidateCodeGenerator imageCodeGenerator() {
            ImageCodeGenerator imageCodeGenerator=new ImageCodeGenerator();
            imageCodeGenerator.setSecurityProperties(securityProperties);
            return imageCodeGenerator;
        }
        
        @Bean
        @ConditionalOnMissingBean(name = "smsCodeGenerator")
        public ValidateCodeGenerator smsCodeGenerator() {
            SmsCodeGenerator smsCodeGenerator=new SmsCodeGenerator();
            smsCodeGenerator.setSecurityProperties(securityProperties);
            return smsCodeGenerator;
        }
        
        @Bean
        @ConditionalOnMissingBean(name = "smsCodeSender")
        public SmsCodeSender smsCodeSender() {
            SmsCodeSender smsCodeSender=new DefaultSmsCodeSender();
            return smsCodeSender;
        }
        
    }
    
    @RestController
    public class ValidateCodeController {
        
        @Autowired
        private Map<String, ValidateCodeProcessor> processorMap;
        
        @GetMapping("/code/{type}")
        public void createSmsCode(HttpServletRequest request,HttpServletResponse response,@PathVariable String type) throws Exception {
            processorMap.get(type+"CodeProcessor").create(new ServletWebRequest(request, response));
        }
        
    }
    
    package com.fuiou.security.core.validate.code;
    
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.bind.ServletRequestBindingException;
    import org.springframework.web.bind.ServletRequestUtils;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import com.fuiou.security.core.properties.SecurityProperties;
    
    import lombok.Setter;
    
    public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{
        
        @Setter
        private AuthenticationFailureHandler authenticationFailureHandler;
        
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
        
        private Set<String> urls=new HashSet<String>();
        
        @Setter
        private SecurityProperties securityProperties;
        
        private AntPathMatcher pathMatcher=new AntPathMatcher();
        
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            String[] configUrls=StringUtils.split(securityProperties.getCode().getSms().getUrls());
            for (String url : configUrls) {
                urls.add(url);
            }
            urls.add("/authentication/mobile");
        }
        
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            boolean action=false;
            for (String url : urls) {
                if(pathMatcher.match(url, request.getRequestURI())) {
                    action=true;
                    break;
                }
            }
            if(action) {
                try {
                    validate(new ServletWebRequest(request));
                } catch (ValidateCodeException e) {
                    authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
                
            }
            filterChain.doFilter(request, response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
             // 从session中获取验证码
            String key=ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS";
            ValidateCode smsCodeInSession = (ValidateCode) sessionStrategy.getAttribute(request,key);
            // 从请求中获取用户填写的验证码
            String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
            if (StringUtils.isBlank(imageCodeInRequest)) {
                throw new ValidateCodeException("验证码不能为空");
            }
            if (null == smsCodeInSession) {
                throw new ValidateCodeException("验证码不存在");
            }
            if (smsCodeInSession.isExpired()) {
                sessionStrategy.removeAttribute(request, key);
                throw new ValidateCodeException("验证码已过期");
            }
            if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, smsCodeInSession.getCode())) {
                throw new ValidateCodeException("验证码不匹配");
            }
            // 验证成功,删除session中的验证码
            sessionStrategy.removeAttribute(request,key);
        }
    }
    
    

    上面就是获取短信验证码和校验短信验证码的逻辑。

    接下来写:短信验证码登录的逻辑。
    为什么不把短信验证和和登录逻辑写在一起呢?因为校验短信验证码的功能除了可能在登陆时用到,其他接口也可能用到。

    表单登录流程.png

    上图就是表单登录的流程:
    回忆一下:
    1、UsernamePasswordAuthenticationFilter会调用AuthenticationManager的authenticate方法,
    2、会调用DaoAuthenticationProvider的supports方法,也就是根据表单信息封装的AuthenticationToken,找到相应的AuthenticationProvider。
    3、provider调用authenticate方法中调用UserDetailsService的loadUserByUsername方法
    4、根据UserDetails封装一个已校验的AuthenticationToken(如果loadUserByUsername找不到相应的user,就抛出UsernameNotFoundException),然后放入SecurityContextHolder

    实现短信验证码登录:
    加入SmsCodeAuthenticationFilter,SmsCodeAuthenticationPorvider,SmsCodeAuthenticationToken

    短信验证码.png
    package com.fuiou.security.core.autnentication.mobile;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.authentication.AuthenticationServiceException;
    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 org.springframework.util.Assert;
    
    public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    
        public static final String FUIOU_FORM_MOBILE_KEY = "mobile";
    
        private String usernameParameter = FUIOU_FORM_MOBILE_KEY;
        private boolean postOnly = true;
    
    
        public SmsCodeAuthenticationFilter() {
            super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
        }
    
        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 = obtainMobile(request);
            if (username == null) {
                username = "";
            }
            username = username.trim();
    
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(username);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //this.setAuthenticationManager(authenticationManager);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    
    
        
        protected String obtainMobile(HttpServletRequest request) {
            return request.getParameter(usernameParameter);
        }
    
        
        protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        
        public void setUsernameParameter(String usernameParameter) {
            Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
            this.usernameParameter = usernameParameter;
        }
    
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getUsernameParameter() {
            return usernameParameter;
        }
    
    }
    
    
    package com.fuiou.security.core.autnentication.mobile;
    
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    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;
    
    import lombok.Setter;
    
    public class SmsCodeAuthenticationPorvider implements AuthenticationProvider{
        @Setter
        private UserDetailsService userDetailsService;
        
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            SmsCodeAuthenticationToken smsCodeAuthenticationToken=(SmsCodeAuthenticationToken)authentication;
            UserDetails user=userDetailsService.loadUserByUsername(smsCodeAuthenticationToken.getPrincipal().toString());
            if(user==null) {
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
            SmsCodeAuthenticationToken token=new SmsCodeAuthenticationToken(user, user.getAuthorities());
            token.setDetails(smsCodeAuthenticationToken.getDetails());
            return token;
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }
    
    
    
    package com.fuiou.security.core.autnentication.mobile;
    
    import java.util.Collection;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    
    public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken{
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
    
        private final Object principal;
        
        public SmsCodeAuthenticationToken(Object principal) {
            super(null);
            this.principal = principal;
            setAuthenticated(false);
        }
    
        public SmsCodeAuthenticationToken(Object principal,
                Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            super.setAuthenticated(true); // must use super, as we override
        }
    
    
        public Object getPrincipal() {
            return this.principal;
        }
    
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            if (isAuthenticated) {
                throw new IllegalArgumentException(
                        "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            }
    
            super.setAuthenticated(false);
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
        }
    
        @Override
        public Object getCredentials() {
            return null;
        }
    }
    
    

    上面的代码都是直接复制的表单登录的源码,然后删除了一些不要的东西,如:密码。

    @Component
    public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>{
        @Autowired
        private AuthenticationSuccessHandler fuiouAuthenticationSuccessHandler;
        
        @Autowired
        private AuthenticationFailureHandler fuiouAuthenticationFailureHandler;
    
        @Autowired
        private UserDetailsService myUserDetailsService;
        
        @Override
        public void configure(HttpSecurity http) throws Exception {
            SmsCodeAuthenticationFilter smsCodeAuthenticationFilter=new SmsCodeAuthenticationFilter();
            smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(fuiouAuthenticationSuccessHandler);
            smsCodeAuthenticationFilter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
        
            SmsCodeAuthenticationPorvider smsCodeAuthenticationPorvider=new SmsCodeAuthenticationPorvider();
            smsCodeAuthenticationPorvider.setUserDetailsService(myUserDetailsService);
            
            http.authenticationProvider(smsCodeAuthenticationPorvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    
    package com.fuiou.security.browser;
    
    
    import javax.sql.DataSource;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    
    import com.fuiou.security.core.autnentication.mobile.SmsCodeAuthenticationFilter;
    import com.fuiou.security.core.autnentication.mobile.SmsCodeAuthenticationSecurityConfig;
    import com.fuiou.security.core.properties.SecurityProperties;
    import com.fuiou.security.core.validate.code.SmsCodeFilter;
    import com.fuiou.security.core.validate.code.ValidateCodeFilter;
    @Configuration
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
        
        
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 如果项目内已经有自己的加密解密逻辑,返回自己的PasswordEncoder
            return new BCryptPasswordEncoder();
        }
        
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
        
        @Autowired
        private SecurityProperties securityProperties;
        
        
        @Autowired
        private AuthenticationSuccessHandler fuiouAuthenticationSuccessHandler;
        
        @Autowired
        private AuthenticationFailureHandler fuiouAuthenticationFailureHandler;
        
        @Autowired
        private DataSource dataSource;
        
        @Autowired
        private UserDetailsService myUserDetailsService;
        
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            tokenRepository.setCreateTableOnStartup(false);
            return tokenRepository;
        }
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter filter=new ValidateCodeFilter();
            filter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
            filter.setSecurityProperties(securityProperties);
            filter.afterPropertiesSet();
            
            
            SmsCodeFilter smsCodefilter=new SmsCodeFilter();
            smsCodefilter.setAuthenticationFailureHandler(fuiouAuthenticationFailureHandler);
            smsCodefilter.setSecurityProperties(securityProperties);
            smsCodefilter.afterPropertiesSet();
            
            http
                .addFilterBefore(smsCodefilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/requrie")//登录页面
                .loginProcessingUrl("/authentication/form")
                .successHandler(fuiouAuthenticationSuccessHandler)
                .failureHandler(fuiouAuthenticationFailureHandler)
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                .userDetailsService(myUserDetailsService)
                .and()
                .authorizeRequests()//请求授权
                .antMatchers("/authentication/requrie",
                            securityProperties.getBrowser().getLoginPage(),
                            "/code/*").permitAll()//该url不需要身份认证
                .anyRequest()       //任何请求
                .authenticated()//身份认证
                .and()
                .csrf().disable()//关闭csrf防护
                .apply(smsCodeAuthenticationSecurityConfig)
                ;
    
        }
        
    }
    
    
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>标准登录页</title>
    </head>
    <body>
        标准登录
        <form action="/authentication/form" method="post">
            <table>
                <tr>
                    <td>用户名</td>
                    <td><input name="username" /></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="password" name="password" /></td>
                </tr>
                <tr>
                    <td>图形验证码</td>
                    <td style="line-height: 35px">
                        <input type="text" name="imageCode" /><img alt="" src="/code/image" >
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="checkbox" value="true" name="remember-me">记住我</button>
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <button type="submit">登录</button>
                    </td>
                </tr>
            </table>
            
        </form>
        
        
        短信登录
        <form action="/authentication/mobile" method="post">
            <table>
                <tr>
                    <td>用户名</td>
                    <td><input name="mobile" value="123456"/></td>
                </tr>
                <tr>
                    <td>短信验证码</td>
                    <td style="line-height: 35px">
                        <input type="text" name="smsCode"/>
                        <a href="/code/sms?mobile=123456">发送短信验证码</a>
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <button type="submit">登录</button>
                    </td>
                </tr>
            </table>
            
        </form>
    </body>
    </html>
    

    重构代码

    相关文章

      网友评论

          本文标题:Spring Security 开发基于表单的认证

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