赞
踩
项目是前后端分离的,前端是vue.js 后端是springboot
1、kaptcha组件用来用户登陆时的一个验证码生成。
2、SpringSecurity安全框架,使用SpringSecurity拦截登陆请求 进行认证和授权,因为是前后端分离的不用做像jsp重定向处理,只用做对应接口授权。
3、登陆业务逻辑是支持账户登陆和邮箱登陆,用户登陆是允许三次输入错误密码,超过三次密码错误,则账号锁定,需要等待指定时间才解锁。
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;
分别是: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方法 }
@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(); } }
下面是登陆验证的主要逻辑,然后有两个关键字段,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("登陆未成功"); }
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>
实现自定义获取用户过程,覆盖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; } }
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(); } }
<!--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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。