前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot集成SpringSecurity - 表单登录添加验证码(四)

SpringBoot集成SpringSecurity - 表单登录添加验证码(四)

作者头像
用户1212940
发布2022-04-13 17:06:55
1.8K0
发布2022-04-13 17:06:55
举报
文章被收录于专栏:LambdaLambda

源码地址:https://github.com/springsecuritydemo/microservice-auth-center04

通过之前文章的学习,我们已经基本上掌握了SpringSecurity的基本流程。你会发现,真正的login请求时有SpringSecurity帮我们处理的,那么我们如何实现自定义表单登录呢,必须添加一个验证码等。

一、添加验证码

我们这里为了方便,直接从百度找了个生成验证码的代码,你也可以使用自己项目中的验证码生成工具。

1.1 生成验证码工具类

代码语言:javascript
复制
public class VerifyCodeUtils {

    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();


    /**
     * 使用系统默认字符源生成验证码
     * @param verifySize    验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize){
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }
    /**
     * 使用指定源生成验证码
     * @param verifySize    验证码长度
     * @param sources   验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources){
        if(sources == null || sources.length() == 0){
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for(int i = 0; i < verifySize; i++){
            verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
        }
        return verifyCode.toString();
    }

    /**
     * 输出指定验证码图片流
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW };
        float[] fractions = new float[colors.length];
        for(int i = 0; i < colors.length; i++){
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h-4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h-4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for(int i = 0; i < verifySize; i++){
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        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);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
        }
    }
}

1.2 编写Redis配置、封装结果集和生成图片接口

我们将生成的验证码存入到服务器的 Session 对象中,但如果你的项目是分布式项目或者是App项目,这里就不能存入到Session中,可以考虑使用 Redis 存储。我们采用Redis 存储方案。 添加redis依赖包:

代码语言:javascript
复制
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

添加redis和图片验证码失效时间:

代码语言:javascript
复制
...
  redis:
    #数据库索引
    database: 0
    host: 127.0.0.1
    port: 6379
    password:
    #连接超时时间
    timeout: 5000
loginCode:
  expiration: 1 #登录验证码过期时间,单位 分钟
  prefix: login_code #验证码redis的key值前缀

编写图片结果集:

代码语言:javascript
复制
@Data
@AllArgsConstructor
public class ImgResult {
    private String img;
    private String uuid;
}

编写获取验证码接口:

代码语言:javascript
复制
    // 登录验证码过期时间:单位 分钟
    @Value("${loginCode.expiration}")
    private Long expiration;

    @Value("${loginCode.prefix}")
    private String prefix;
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 获取验证码
     */
    @GetMapping("/vCode")
    @ResponseBody
    public ImgResult getCode() throws IOException {

        // 生成随机字串
        String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
        String uuid = UUID.randomUUID().toString();
        // 存入redis
        redisTemplate.opsForValue().set(prefix + uuid,verifyCode, expiration, TimeUnit.MINUTES);
        // 生成图片
        int w = 111, h = 36;
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        VerifyCodeUtils.outputImage(w, h, stream, verifyCode);
        try {
            return new ImgResult(Base64.encode(stream.toByteArray()),uuid);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            stream.close();
        }
    }

这里采用 Base64格式的图片返回,使用Hutool依赖包完成Base64转换:

代码语言:javascript
复制
        <!--工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.11</version>
        </dependency>

1.3 修改login.html

在原来的 login 页面集成上加入 验证码字段:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
    <script src="js/jquery-3.4.1.min.js"></script>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
    <div>
        用户名:<input type="text" name="username">
    </div>
    <div>
        密 码:<input type="password" name="password">
    </div>
    <div>
        验证码:<input type="text" class="form-control" name="verifyCode" required="required" placeholder="验证码">
        <input id="uuid" type="hidden" name="uuid" />
        <img  id="vCode" title="看不清,请点我" onclick="getVerifyCode()" onmouseover="mouseover(this)" />
    </div>
    <div>
        <label><input type="checkbox" name="remember-me"/>自动登录</label>
    </div>
    <div>
        <button type="submit">立即登陆</button>
    </div>
</form>

<script>
    $(function() {
        getVerifyCode();
    })

    function getVerifyCode() {
        var url = "/vCode?" + Math.random();
        $.ajax({
            //请求方式
            type : "GET",
            //请求的媒体类型
            contentType: "application/json;charset=UTF-8",
            //请求地址
            url : url,
            //请求成功
            success : function(result) {
                console.log(result);
                $("#uuid").val(result.uuid);
                $("#vCode").attr("src","data:image/png;base64," + result.img);
            },
            //请求失败,包含具体的错误信息
            error : function(e){
                console.log(e.status);
                console.log(e.responseText);
            }
        });
    }

    function mouseover(obj) {
        obj.style.cursor = "pointer";
    }
</script>

</body>
</html>

1.4 添加匿名访问 URL(放行 验证码请求)

WebSecurityConfig 中允许 验证码请求匿名访问,不然没有登录就没办法获取验证码(死循环了)。

代码语言:javascript
复制
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${loginCode.prefix}")
    private String prefix;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 设置登陆成功url
                .defaultSuccessUrl("/").permitAll()
                // 设置登录失败url
                .failureUrl("/login/error")
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                // 添加图片验证码过滤器
                .addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效时间,单位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
    }

这样验证码就加好了。

1.5 运行程序

11464886-796f4fc43f24e941.png
11464886-796f4fc43f24e941.png

二、验证码验证

验证方式:

  • AJAX 验证
  • 过滤器验证
  • Spring Security 验证 接下来我们分别针对这几种验证方式做讲解。

2.1 AJAX验证

使用 AJAX 方式验证和我们 Spring Security 框架就没有任何关系了,其实就是表单提交前先发个 HTTP 请求验证验证码,本篇不再赘述。有兴趣的同学可以自己实现。

2.2 过滤器验证

使用过滤器验证的思路: 在SpringSecurity 处理登录验证请求前,先验证验证码,如果正确,放行;如果不正确,抛出异常。

具体实现步骤如下:

第一步:编写自定义验证码异常,继承AuthenticationException抽象类
代码语言:javascript
复制
public class VerifyCodeException extends AuthenticationException {

    public VerifyCodeException(String msg) {
        super(msg);
    }

    public VerifyCodeException(String msg, Throwable t) {
        super(msg, t);
    }
}
第二步:编写验证码过滤器

自定义一个过滤器,实现 OncePerRequestFilter(用于防止多次执行Filter的;也就是说一次请求只会走一次拦截器链) ,在 isProtectedUrl() 方法中拦截 POST 方式的/login 请求。

在逻辑处理中从 request 中取出验证码,并进行验证,如果验证成功,放行;验证失败,手动抛出异常。

代码语言:javascript
复制
public class VerifyFilter extends OncePerRequestFilter{

    private static final PathMatcher pathMatcher = new AntPathMatcher();

    private StringRedisTemplate stringRedisTemplate;

    private String prefix;

    public VerifyFilter() {}

    public VerifyFilter(StringRedisTemplate stringRedisTemplate, String prefix) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.prefix = prefix;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 拦截 /login的POST请求
        if(isProtectedUrl(request)) {
            String uuid = request.getParameter("uuid"); // 图片验证码的 key
            String vCode = request.getParameter("verifyCode");// 图片验证码的 value

            if(!validateVerify(request, uuid, vCode)) {
                //手动设置异常
                request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new VerifyCodeException("验证码输入错误"));
                // 转发到错误Url
                request.getRequestDispatcher("/login/error").forward(request,response);
            } else {
                filterChain.doFilter(request,response);
            }
        } else {
            filterChain.doFilter(request,response);
        }
    }

    /**
     * 验证验证码合法性
     * @param uuid  验证key
     * @param vCode 验证值
     * @return
     */
    private boolean validateVerify(HttpServletRequest request, String uuid, String vCode) {
        // 查询验证码
        String code = stringRedisTemplate.opsForValue().get(prefix + uuid);

        // 清除验证码
        stringRedisTemplate.delete(prefix + uuid);

        if (StringUtils.isBlank(code)) {
            //手动设置异常
            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("验证码已过期"));
            return false;
        }
        if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new VerifyCodeException("验证码错误"));
            return false;
        }

        logger.info("验证码:" + code + "用户输入:" + vCode);
        return true;
    }

    // 拦截 /login的POST请求
    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
    }

}
第三步:修改loginError()方法,添加 图片验证码异常处理 :

特别注意:这里不要指定请求方式,而使用:@RequestMapping("/login/error"),这里之前我做这个测试,发现SpringSecurity 默认错误调整使用的是 GET 方式,这里我们手动通过 request.getDispatcher("/login/error").forward()使用的是 post方式。所以这里一定记得修改注解方式为 @RequestMapping(),不然你会发现怎么也不成功。

11464886-53593974af80dc0b.png
11464886-53593974af80dc0b.png
代码语言:javascript
复制
    @RequestMapping("/login/error")
    @ResponseBody
    public Result loginError(HttpServletRequest request) {
        AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        log.info("authenticationException={}", authenticationException);
        Result result = new Result();
        result.setCode(201);

        // 图片验证码校验
        if(authenticationException instanceof VerifyCodeException) {
            result.setMsg(authenticationException.getMessage());
        } else if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
            result.setMsg("用户名或密码错误");
        } else if (authenticationException instanceof DisabledException) {
            result.setMsg("用户已被禁用");
        } else if (authenticationException instanceof LockedException) {
            result.setMsg("账户被锁定");
        } else if (authenticationException instanceof AccountExpiredException) {
            result.setMsg("账户过期");
        } else if (authenticationException instanceof CredentialsExpiredException) {
            result.setMsg("证书过期");
        } else {
            result.setMsg("登录失败");
        }
        return result;
    }
第四步:注入过滤器

修改 WebSecurityConfigconfigure() 方法,添加一个 addFilterBefore(),具有两个参数,作用是在参数二之前执行参数一指定的过滤器。

SpringSecurity 对于用户名/密码登录方式是通过 UsernamePasswordAuthenticationFilter 处理的,所以我们在它之前执行自定义验证码过滤器即可。

代码语言:javascript
复制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 设置登陆成功url
                .defaultSuccessUrl("/").permitAll()
                // 设置登录失败url
                .failureUrl("/login/error")
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                // 添加图片验证码过滤器
                .addFilterBefore(new VerifyFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效时间,单位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
第五步:运行程序
11464886-547b889e73adf15a.gif
11464886-547b889e73adf15a.gif

上面我们使用过滤器实现了验证功能,但是其他它和AJAX 验证差别不大。

  • AJAZ 验证是在登录提交前发送一个异步请求,请求返回成功就提交登录;失败就不提交登录。
  • 过滤器是先验证验证码,验证成功就让 SpringSecurity 验证用户名和密码;验证失败则抛出异常。

如果我们要做的需求是用户登录时需要多个验证字段,不单单是用户名和密码,那么使用过滤器会让逻辑变得复杂,而这里我们通过另外一种方式来完整验证逻辑。

2.3 SpringSecurity验证

第一步:自定义 WebAuthenticationDetails 类:

我们知道SpringSecurity 默认情况下只会处理用户名和密码信息。

WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息。

代码语言:javascript
复制
public class WebAuthenticationDetails implements Serializable {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final String remoteAddress;
    private final String sessionId;
    ...

这时候我们就要自定义 CustomWebAuthenticationDetails类,并在其中加入我们的验证码字段:

代码语言:javascript
复制
package com.thtf.auth.security;

import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * ========================
 * 获取用户登录时携带的额外信息
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/24 16:50
 * Version: v1.0
 * ========================
 */
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final String uuid;          //验证码key
    private final String verifyCode;    //验证码value


    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        this.uuid = request.getParameter("uuid");
        this.verifyCode = request.getParameter("verifyCode");
    }

    public String getUuid() {
        return uuid;
    }

    public String getVerifyCode() {
        return verifyCode;
    }
}

在这个类我们增加两个属性:uuid 和 verifyCode。

第二步:配置 AuthenticationDetailsSource

自定义了 WebAuthenticationDetails,我们需要将其放入到 AuthenticationDetailsSource 中替换原来的 WebAuthenticationDetails 对象,所以我们还得实现自定义 AuthenticationDetailsSource

代码语言:javascript
复制
package com.thtf.auth.security;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * ========================
 * 该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/24 17:06
 * Version: v1.0
 * ========================
 */
@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
}
第三步:将 CustomAuthenticationDetailsSource 注入到SpringSecurity中。

修改 WebSecurityConfig,在 configure() 方法中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它,替换默认的AuthenticationDetailsSource对象。

代码语言:javascript
复制
    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .antMatchers("/vCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 设置登陆成功url
                .defaultSuccessUrl("/").permitAll()
                // 设置登录失败url
                .failureUrl("/login/error")
                // 自定义登陆用户名和密码参数,默认为username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                // 指定authenticationDetailsSource
                .authenticationDetailsSource(authenticationDetailsSource)
                .and()
                // 添加图片验证码过滤器
                //.addFilterBefore(new VerifyFilter(redisTemplate, prefix), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                // 自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效时间,单位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
    }
第四步:自定义 AuthenticationProvider

上面我们通过自定义 WebAuthenticationDetailsAuthenticationDetailsSource将验证码key、验证码值和用户名、密码一起带入了Spring Security中,下面我们需要将它取出来。

这里需要我们自定义AuthenticationProvider,需要注意:如果是我们自己实现AuthenticationProvider,那么我们就需要自己做密码校验了

代码语言:javascript
复制
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private StringRedisTemplate redisTemplate;


    @Value("${loginCode.prefix}")
    private String prefix;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();

        String uuid = details.getUuid();
        String vCode = details.getVerifyCode();

        // 查询验证码
        String code = redisTemplate.opsForValue().get(prefix + uuid);

        // 清除验证码
        redisTemplate.delete(prefix + uuid);

        if (StringUtils.isBlank(code)) {
            throw new VerifyCodeException("验证码已过期");
        }
        if (StringUtils.isBlank(vCode) || !vCode.equalsIgnoreCase(code)) {
            throw new VerifyCodeException("验证码错误");
        }


        // userDetails为数据库中查询到的用户信息
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

        // 如果是自定义AuthenticationProvider,需要手动密码校验
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        if(!bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }

        return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 这里不要忘记,和UsernamePasswordAuthenticationToken比较
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
第五步: 在 WebSecurityConfig 中注入 CustomAuthenticationProvider:
代码语言:javascript
复制
@Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        auth.authenticationProvider(customAuthenticationProvider);
    }
第六步:运行程序
11464886-9666cf7b2fb11f9d.gif
11464886-9666cf7b2fb11f9d.gif

是不是更复杂了O(∩_∩)O哈哈~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-07-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、添加验证码
    • 1.1 生成验证码工具类
      • 1.2 编写Redis配置、封装结果集和生成图片接口
        • 1.3 修改login.html
          • 1.4 添加匿名访问 URL(放行 验证码请求)
            • 1.5 运行程序
            • 二、验证码验证
              • 2.1 AJAX验证
                • 2.2 过滤器验证
                  • 第一步:编写自定义验证码异常,继承AuthenticationException抽象类
                  • 第二步:编写验证码过滤器
                  • 第三步:修改loginError()方法,添加 图片验证码异常处理 :
                  • 第四步:注入过滤器
                  • 第五步:运行程序
                • 2.3 SpringSecurity验证
                  • 第一步:自定义 WebAuthenticationDetails 类:
                  • 第二步:配置 AuthenticationDetailsSource
                  • 第三步:将 CustomAuthenticationDetailsSource 注入到SpringSecurity中。
                  • 第四步:自定义 AuthenticationProvider
                  • 第五步: 在 WebSecurityConfig 中注入 CustomAuthenticationProvider:
                  • 第六步:运行程序
              相关产品与服务
              验证码
              腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档