springsecruity生成图形验证码并校验

根据随机数生成图片
将随机数存到session中
在将生成的图片写到接口的响应

1. 创建项目

使用idea中的spring工具引入springbootspringsecruity创建项目最终的pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo-spring-secruity</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-spring-secruity</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-web</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

创建一个基本访问路由

package com.example.demospringsecruity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 简单测试控制器
 * @author john
 * @date 2020/1/6 - 9:47
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello spring secruity";
    }
}

创建一个简单的响应对象

package com.example.demospringsecruity.support;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 *  一个简单的响应对象
 * @author john
 * @date 2020/1/6 - 16:23
 */
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class SimpleResponse {
    private Object content;
}

实现图形验证码的校验,需要在用户和密码认证过滤器前增加一个验证码过滤器处理判断逻辑

2. 创建验证码类

package com.example.demospringsecruity.utils;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * 验证码对象
 *
 * @author john
 * @date 2020/1/7 - 9:54
 */
@Data
@ToString
@NoArgsConstructor
public class ImageCode {
    //验证码
    private String code;
    //过期时间
    private LocalDateTime expireTime;
    //图片
    private BufferedImage image;

    public ImageCode(String code, int expireTime, BufferedImage image) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
        this.image = image;
    }

    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}

3. 编写验证码的过滤器

package com.example.demospringsecruity.filter;

import com.example.demospringsecruity.controller.ValidateCodeController;
import com.example.demospringsecruity.exception.ValidateCodeException;
import com.example.demospringsecruity.handler.MyAuthenticationFailureHandler;
import com.example.demospringsecruity.utils.ImageCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author john
 * @date 2020/1/7 - 10:41
 */
@Slf4j
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {


    MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    public MyAuthenticationFailureHandler getMyAuthenticationFailureHandler() {
        return myAuthenticationFailureHandler;
    }

    public void setMyAuthenticationFailureHandler(MyAuthenticationFailureHandler myAuthenticationFailureHandler) {
        this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
    }

    /**
     * 操作session的工具类
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        //只有当处理登录功能时才执行
        if (StringUtils.equals("/auth/form", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
            // 最好做try catch 处理异常
            try {
                validate(new ServletWebRequest(request));
            } catch (AuthenticationException e) {
                myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    //执行验证码逻辑
    private void validate(ServletWebRequest request) throws AuthenticationException, ServletRequestBindingException {
        //取出session中的验证码对象
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        //取出表单提交中的验证码信息
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("code in request is null");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException("code in session is null");
        }

        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("code has expreied");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("code is not right");
        }

        //验证通过,清除session中的验证码信息
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }
}

4. 定义一个自定义登录错误处理

package com.example.demospringsecruity.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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;

/**
 * 自定义登录失败处理
 *
 * @author john
 * @date 2020/1/6 - 18:43
 */
@Component
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.info("登录失败");
        //这里处理登录失败后就会输出错误信息
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset-UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(e.getMessage()));
    }
}

5. 自定义验证码异常

package com.example.demospringsecruity.exception;

import org.springframework.security.core.AuthenticationException;

/**
 *  自定义异常
 * @author john
 * @date 2020/1/7 - 11:25
 */
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String msg) {
        super(msg);
    }
}

6. 定义验证码图片输出控制器

package com.example.demospringsecruity.controller;

import com.example.demospringsecruity.utils.ImageCode;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/**
 * 验证码校验控制器
 *
 * @author john
 * @date 2020/1/7 - 9:57
 */
@RestController
public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(request);
        //将验证码存入session
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        //输出图片
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    //创建验证码对象
    private ImageCode createImageCode(HttpServletRequest request) {
        int width = 67;
        int height = 23;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageCode(sRand, 60, image);
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

}

7. 配置登录逻辑以及添加自定义的验证码过滤器到用户密码过滤器前面

package com.example.demospringsecruity.config;

import com.example.demospringsecruity.filter.ValidateCodeFilter;
import com.example.demospringsecruity.handler.MyAuthenticationFailureHandler;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;

/**
 * @author john
 * @date 2020/1/6 - 10:07
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    ValidateCodeFilter validateCodeFilter;
    @Autowired
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;


    //手动将PasswordEncoder注入到ioc容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        validateCodeFilter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
        // 表单登录
        http    //过滤器设置
                // 将验证码过滤器配置到UsernamePasswordAuthenticationFilter前面
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                //登录设置
                .formLogin()
                .loginPage("/signin.html")     //设置登录路由
                .loginProcessingUrl("/auth/form")  //设置登录处理url
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                // 身份认证设置
                .authorizeRequests()
                .antMatchers("/signin.html").permitAll() //该路由不需要身份认账
                .antMatchers("/code/*").permitAll() //该路由不需要身份认账
                .anyRequest()       //其他的路由均需要身份认证
                .authenticated()
                .and()
                //先禁用防止跨站脚本攻击的csrf token
                .csrf()
                .disable();
    }

}

8. 自定义用户认证

package com.example.demospringsecruity.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @author john
 * @date 2020/1/6 - 10:32
 */
@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
    //这里可以注入mapper或者repository的dao对象来实现数据校验逻辑操作
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("用户名:" + username);
        //这里密码应该从数据库中取出,暂时先使用加密生成
        String password = passwordEncoder.encode("123456");
        return new User(username,
                password,
                true,                 // 账户是否可用
                true,        // 账户是否过期
                true,     // 密码是否过期
                true,        //账户是否被锁定
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin") //授权集合
        );
    }
}

9. 静态登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/auth/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>图形验证码</td>
            <td><input type="text" name="imageCode"><img src="/code/image" alt="图形验证码"></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

10. 测试

验证码错误结果

验证码正确,用户密码校验通过结果

11. 代码资源

链接:https://share.weiyun.com/5jKnFRj 密码:5vvs5x

猜你喜欢

转载自www.cnblogs.com/ifme/p/12160893.html