基于springboot+SpringSecurity+kaptcha的用户登陆 密码三次输入错误锁定,登陆成功则授权。

项目是前后端分离的,前端是vue.js 后端是springboot
1、kaptcha组件用来用户登陆时的一个验证码生成。
2、SpringSecurity安全框架,使用SpringSecurity拦截登陆请求 进行认证和授权,因为是前后端分离的不用做像jsp重定向处理,只用做对应接口授权。
3、登陆业务逻辑是支持账户登陆和邮箱登陆,用户登陆是允许三次输入错误密码,超过三次密码错误,则账号锁定,需要等待指定时间才解锁。

一、首先基于MySql数据库表的设计

CREATE TABLE `user_info` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(500) DEFAULT '' COMMENT '密码',
  `nick_name` varchar(20) DEFAULT NULL COMMENT '用户昵称',
  `sex` int(11) DEFAULT '0' COMMENT '性别(默认 0:未知 1:女 2:男)',
  `role` varchar(20) DEFAULT '' COMMENT '角色',
  `allow_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '允许登陆时间',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `error_num` int(11) DEFAULT NULL COMMENT '登录错误次数',
  `status` tinyint(4) DEFAULT '1' COMMENT '账户状态(默认1:可用 0:锁定)',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

二、用户实体类的设计,实现SpringSecurity提供的UserDetails接口,需要覆盖接口对应的方法,

分别是:getAuthorities()->获取权限,isAccountNonExpired()->账户是否过期,isAccountNonLocked()->账户是否锁定,isCredentialsNonExpired()->授权是否过期,isEnabled()->是否可用。
下面省略了get和set方法。

public class UserVO implements UserDetails, Serializable {
    private static final long serialVersionUID = -962358173215433342L;
    private String id; // 用户ID 
    private String username; //用户账户
    private String email; // 邮箱 
    private String password;  // 用户密码 
    private Integer roleId; // 用户角色ID 
    private String roleName; // 用户角色名称 
    private String kaptcha; //验证码 
    private Integer errorNum;  // 用户登陆错误次数 
    private Date allowTime; // 用户允许登陆时间 
    HashMap<String,HashMap<String,Boolean>> authoritiesMap; //当前用户具备权限
    private List<? extends GrantedAuthority> authorities; //用户权限

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
    //此处省略成员变量的get和set方法
}

三、控制层Controller,一个登陆接口和一个生成验证码接口,CHECK_CODE是定义的session的值,用于存放验证码,校验验证码正确后再执行service层的登陆逻辑。

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    DefaultKaptcha defaultKaptcha;

    /**
     * 用户登陆
     *
     * @param vo
     * @return
     */
    @PostMapping("/login")
    public ResultVO userLogin(@RequestBody UserVO vo, HttpServletRequest request) {
        String s = request.getSession().getAttribute(ConstantVal.CHECK_CODE).toString();
        if (StringUtils.isEmpty(vo.getKaptcha()) || !s.equals(vo.getKaptcha())) {      //验证码为空或者错误
            return ResultVO.error("验证码有误");
        }
        return userService.userLogin(vo, request);
    }

        /**
     * 生成验证码
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @throws Exception
     */
    @RequestMapping("/defaultKaptcha")
    public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        byte[] captchaChallengeAsJpeg = null;
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            // 生产验证码字符串并保存到session中
            String createText = defaultKaptcha.createText();
            httpServletRequest.getSession().setAttribute(ConstantVal.CHECK_CODE, createText);
            BufferedImage challenge = defaultKaptcha.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (IllegalArgumentException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-store");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }
}

四、service层的设计,主要的登录逻辑这这部分。首先校验账号密码,基于既可以账号登陆又可以邮箱登陆,账号和邮箱是不同字段,先基于账号查账号字段,不存在则基于账号查邮箱字段。

下面是登陆验证的主要逻辑,然后有两个关键字段,errorNum错误次数,allowTime用户允许登陆的时间,
1、首先用户允许登陆时间为空或者当前时间大于允许登陆时间,则做进一步判断,否则返回账号锁定,还没到允许登录的时间。
2、如果允许登陆做进一步校验,这里密码采用加密加盐判断校验,如果密码不正确,则判断错误次数是否到达3次,用errorNum这个字段记录,如果超过3次,allowTime更新到定义登陆的时候,即锁定。如果错误次数没有超过3次,那么再进行判断是否是否距离上次输入错误密码间隔的时间是2分钟之内的。如果间隔时间2分钟内,则错误次数errorNum再次加1。
3、如果账号密码正确,则进行授权加载对应的授权规则返回给前端。

    public ResultVO userLogin(UserVO vo, HttpServletRequest request) {

        if (StringUtil.isBlank(vo.getUsername()) || StringUtil.isBlank(vo.getPassword())) {
            return ResultVO.error("账户或密码为空");
        }
        UserVO queryVo = userMapper.queryUserByUserName(vo);
        if (queryVo == null) {
            queryVo = userMapper.loadUserByUserEmail(vo.getUsername());
            if (queryVo == null) {
                return ResultVO.error("账户不存在");
            }
        }
        Date allowTime = queryVo.getAllowTime();   //允许登陆时间
        Date currentTime = new Date();            //当前时间

        if (allowTime == null || currentTime.getTime() > allowTime.getTime()) {    //如果当前登录时间大于允许登录时间
            UserVO user = new UserVO();
            user.setUsername(queryVo.getUsername());
            if (!MD5Util.getSaltverifyMD5(vo.getPassword(), queryVo.getPassword())) {  //如果账号密码错误
                int errorNum = queryVo.getErrorNum() == null ? 0 : queryVo.getErrorNum(); //登录错误次数
                long allowTimes = queryVo.getAllowTime() == null ? 0 : queryVo.getAllowTime().getTime();
                if (errorNum < 2) {			//错误的次数
                    int result;
                    if ((currentTime.getTime() - allowTimes) <= 120000) {    //每次输入错误密码间隔时间在2分钟内 (如果上次登录错误时间距离相差小于定义的时间(毫秒))
                        user.setErrorNum(errorNum + 1);
                        user.setAllowTime(new Date());
                        result = userMapper.updateUser(user);
                    } else {
                        user.setErrorNum(1);
                        user.setAllowTime(new Date());
                        result = userMapper.updateUser(user);
                    }
                    if (result == 1) {
                        return ResultVO.error("账号密码错误");
                    }
                } else {
                    Date dateAfterAllowTime = new Date(currentTime.getTime() + 600000);     //锁定10分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
                    user.setErrorNum(0);   //错误次数清零
                    user.setAllowTime(dateAfterAllowTime);
                    int result = userMapper.updateUser(user);
                    if (result == 1) {
                        return ResultVO.error("用户账号密码输入三次失败,被锁定十分钟");
                    }
                }
            } else {        //账号密码正确
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(queryVo.getUsername(), queryVo.getPassword());
                try {
                    //使用SpringSecurity拦截登陆请求 进行认证和授权
                    Authentication authenticate = myAuthenticationManager.authenticate(usernamePasswordAuthenticationToken);

                    SecurityContextHolder.getContext().setAuthentication(authenticate);
                    //使用redis session共享
                    HttpSession session = request.getSession();
                    session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
                } catch (Exception e) {
                    e.printStackTrace();
                    return ResultVO.error("登陆异常");
                }
                user.setErrorNum(0);
                int result = userMapper.updateUser(user);
                if (result == 1) {
                    HashMap<String, HashMap<String, Boolean>> userAuthroityList = getUserAuthroityList(queryVo);	//这块的话是取出当前用户对那些接口操作有权限,这块可以根据不同需求做。
                    queryVo.setAuthoritiesMap(userAuthroityList);
                    return ResultVO.success(queryVo);
                }
            }
        } else {
            return ResultVO.error("账号锁定,还没到允许登录的时间");
        }
        return ResultVO.error("登陆未成功");
    }

五、DAO层的设计,DAO数据交互层也很简单,查询用户信息和更新用户信息而已。

1、UserVO queryUserByUserName(UserVO vo); //通过用户账户查询用户信息
2、int updateUser(UserVO vo); // 更新用户

      <select id="queryUserByUserName" resultType="com.game.manager.security.domain.UserVO">
        SELECT
            username,
            password,
            role,
            status,
            allow_at as allowTime,
            error_num as errorNum
        FROM
            user_info
        WHERE
            username = #{username}
    </select>

        <update id="updateUser" parameterType="com.game.manager.security.domain.UserVO">
        UPDATE
        user_info
        <set>
            <if test="errorNum != null">
                error_num = #{errorNum},
            </if>
            <if test="allowTime != null">
                allow_at = #{allowTime,jdbcType=TIMESTAMP},
            </if>
        </set>
        WHERE
        username = #{username}
    </update>

六、SpringSecurity认证和授权部分,这块可以根据每个人的需要授权的接口进行设计。这里的话是实现SpringSecurity提供的UserDetailsService接口,

实现自定义获取用户过程,覆盖UserDetailsService的loadUserByUsername方法,通过username来获取user信息。然后获取到自己定义好的权限列表然后存储到SpringSecurity提供的GrantedAuthority里并设置给user进行授权。

 @Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //认证账号
        UserVO user = userMapper.loadUserByUsername(s);
        if(user == null){
            user = userMapper.loadUserByUserEmail(s);
            if(user == null) {
                throw new UsernameNotFoundException("账号不存在");
            }
        }
        //对用户的角色进行授权
       List<RoleMenuVO> roleMenuVOS = userMapper.queryUserAuthroityList(user);
       List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

       if(roleMenuVOS.size() == 1 && roleMenuVOS.get(0) == null){
           return user;
       }
        for(RoleMenuVO roleMenu : roleMenuVOS){
            if(!StringUtil.isBlank(roleMenu.getUrl())) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(roleMenu.getUrl());
                //此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                grantedAuthorities.add(grantedAuthority);
            }
        }
        user.setAuthorities(grantedAuthorities);
        return user;
    }
}

七、config配置,配置部分有两块,第一块的话是Kaptcha验证码组件的配置,第二块的话是SpringSecurity的配置。

1、kaptcha配置也简单,对象装配后,配置里面的属性,验证码的字体调色模糊度等,属性有很多,大家可以去官网查阅,这里不一一介绍。

@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha defaultKaptcha(){
        com.google.code.kaptcha.impl.DefaultKaptcha defaultKaptcha = new com.google.code.kaptcha.impl.DefaultKaptcha();
        Properties properties = new Properties();     
        properties.setProperty("kaptcha.border", "no");  // 图片边框     
        properties.setProperty("kaptcha.border.color", "105,179,90"); // 边框颜色
        properties.setProperty("kaptcha.textproducer.font.color", "red"); // 字体颜色   
        properties.setProperty("kaptcha.image.width", "110");  // 图片宽
        properties.setProperty("kaptcha.image.height", "40"); // 图片高
        properties.setProperty("kaptcha.textproducer.font.size", "30");  // 字体大小 
        properties.setProperty("kaptcha.session.key", "code"); // session key
        properties.setProperty("kaptcha.textproducer.char.length", "4"); // 验证码长度
        properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); // 字体
        properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.ShadowGimpy");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

2、SpringSecurity的配置,首先通过@EnableWebSecurity注解开启Spring Security的功能,然后继承WebSecurityConfigurerAdapter类,在configure方法里做拦截配置,
这里的只对登录的接口和验证码接口放行不做拦截,其他接口都需要登录授权。因为SpringSecurity在不授权情况下返回403,这里是因为需求所以定义返回401状态码。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws  Exception{
        auth.userDetailsService(customUserDetailsService())
                .passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                //验证码和登陆不进行拦截
                .antMatchers("/user/defaultKaptcha").permitAll()
                .antMatchers("/user/login").permitAll()
                .anyRequest()
                .authenticated()  //其他的需要登陆后才能访问
                .and()
                .cors()
                .and()
                .csrf().disable()// 取消跨站请求伪造防护
                .exceptionHandling().authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {         //认证失败指定编码401
                    httpServletResponse.setStatus(401);
                    httpServletResponse.setCharacterEncoding("UTF-8");
                    httpServletResponse.setContentType("application/json; charset=utf-8");
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    String body = "{\"status\":\"failure\",\"msg\":" + HttpStatus.UNAUTHORIZED + "}";
                    printWriter.write(body);
                    printWriter.flush();
                });
    }

    /**
     * 自定义UserDetailsService,授权
     * @return
     */
    @Bean
    public CustomUserDetailsService customUserDetailsService(){
        return new CustomUserDetailsService();
    }

    /**
     * AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

八、里面引入的pom.xml的jar包

        <!--kaptcha验证码生成器-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
        <!--SpringSecurity安全框架-->
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
Logo

前往低代码交流专区

更多推荐