注册
在数据库中手动维护用户信息是不合理的,同时数据库中存储明文密码也很容易出问题,因此注册接口是一个必须的接口
代码实现
- Login
package com.learn.security.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NonNull;
/**
* @author PC
* 登录信息
*/
@Data
@TableName("login")
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Login {
public static final String FIELD_ID = "id";
public static final String FIELD_LOGIN_NAME = "login_name";
public static final String FIELD_PASSWORD = "password";
public Login() {
}
@TableId(type = IdType.AUTO)
private Long id;
@NonNull
private String loginName;
@NonNull
private String password;
}
- RegisterDTO
package com.learn.security.api.dto;
import lombok.Data;
/**
* @author PC
* 注册DTO
*/
@Data
public class RegisterDTO {
/**
* 登录名
*/
private String loginName;
/**
* 密码
*/
private String password;
/**
* 验证密码
*/
private String verifyPassword;
/**
* 是否注册成功,1成功0失败
*/
private Integer successFlag;
}
- LoginController
package com.learn.security.api.controller;
import com.learn.security.api.dto.RegisterDTO;
import com.learn.security.app.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author PC
* 登录注册Controller
*/
@RestController("loginController.v1")
@RequestMapping("/v1/login")
public class LoginController {
private LoginService loginService;
@Autowired
public void setLoginService(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping("/register")
public ResponseEntity<RegisterDTO> register(@RequestBody RegisterDTO registerDTO) {
return ResponseEntity.ok(loginService.register(registerDTO));
}
}
- LoginService
/**
* 注册
* @param registerDTO 注册信息
* @return 注册结果
*/
RegisterDTO register(RegisterDTO registerDTO);
- LoginServiceImpl
@Override
@Transactional
public RegisterDTO register(RegisterDTO registerDTO) {
boolean validFlag = !(Objects.isNull(registerDTO) || StringUtils.isEmpty(registerDTO.getLoginName()) || StringUtils.isEmpty(registerDTO.getPassword())
|| StringUtils.isEmpty(registerDTO.getVerifyPassword()));
//必要信息缺失
Assert.isTrue(validFlag, "error.necessary.information.missing");
//两次密码不一致
Assert.isTrue(StringUtils.equals(registerDTO.getPassword(), registerDTO.getVerifyPassword()), "error.auth_password.wrong");
//数据库中是否有这个loginName
Assert.isNull(this.getLoginInfo(registerDTO.getLoginName()), String.format("error.loginName.exist:%s", registerDTO.getLoginName()));
//密码加密存储
try {
registerDTO.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
} catch (Exception exception) {
LOGGER.error("encode password failed{}", exception.getMessage());
registerDTO.setSuccessFlag(0);
return registerDTO;
}
Login newLogin = new Login();
BeanUtils.copyProperties(registerDTO, newLogin);
loginMapper.insert(newLogin);
registerDTO.setSuccessFlag(1);
return registerDTO;
}
- SecurityAutoConfiguration(register接口不用授权即可访问)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//自定义登录页面
.formLogin()
//登陆页面设置
.loginPage("/login.html")
//登录url设置
.loginProcessingUrl("/user/login")
//登录成功后跳转的路径,如果希望跳回原路径,alwaysUse不填或填false
.defaultSuccessUrl("/success.html")
//允许访问
.permitAll()
//配置登出
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll()
//未获得授权跳转的页面
.and().exceptionHandling().accessDeniedPage("/un-auth.html")
//设置认证权限
.and().authorizeRequests()
// /v1/**的没有权限注解的全部放行
.antMatchers("/v1/**").permitAll()
.anyRequest().authenticated()
//关闭csrf防护
.and().csrf().disable();
return http.build();
}
- SecurityConfigProperties
package com.learn.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.regex.Pattern;
/**
* @author PC
* Security配置属性
*/
@Data
@Configuration
@ConfigurationProperties("cus.security")
public class SecurityConfigProperties {
/**
* 角色前缀
*/
private String rolePrefix = "ROLE_";
/**
* 加密算法的正则,默认是BCryptPasswordEncoder
*/
private Pattern encryptPattern = Pattern.compile("\\A\\$2([ayb])?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
}
- UserDetailsServiceImpl
package com.learn.security.app.service.impl;
import com.learn.security.app.service.LoginService;
import com.learn.security.config.SecurityConfigProperties;
import com.learn.security.domain.entity.Login;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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;
import org.springframework.util.Assert;
import java.util.List;
import java.util.regex.Matcher;
/**
* @author PC
* UserDetails实现类
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private LoginService loginService;
private PasswordEncoder passwordEncoder;
private SecurityConfigProperties securityConfigProperties;
@Autowired
public void setLoginService(LoginService loginService) {
this.loginService = loginService;
}
@Autowired
private void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Autowired
private void setSecurityConfigProperties(SecurityConfigProperties securityConfigProperties) {
this.securityConfigProperties = securityConfigProperties;
}
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
// 1. 查询用户
Login loginInfo = loginService.getLoginInfo(loginName);
if (loginInfo == null) {
//这里找不到必须抛异常
throw new UsernameNotFoundException("User " + loginName + " was not found in db");
}
//2. 获取并加密密码
Assert.isTrue(StringUtils.isNotEmpty(loginInfo.getPassword()), "password deletion");
String password = loginInfo.getPassword();
//数据库中可能有一些未加密的密码(之前测试用),这部分在校验的时候需要加密校验,其实可以在注册的时候加一下校验
Matcher matcher = securityConfigProperties.getEncryptPattern().matcher(loginInfo.getPassword());
if (!matcher.matches()) {
password = passwordEncoder.encode(loginInfo.getPassword());
}
//3. 赋予user账户一个或多个权限,用逗号分隔,测试可以使用用户名,正式需添加权限字段
List<GrantedAuthority> grantedAuthorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(loginName);
return new User(loginName, password, grantedAuthorityList);
}
}
- SecurityApplication
@MapperScan("com.learn.security.infra.mapper")
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
测试
- 测试注册接口
- 测试异常情况
可以看到,接口返回结果并不是我们想要的报错信息,因此需要一个异常处理类
- 异常处理类
package com.learn.security.infra.handle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* @author PC
*/
@RestControllerAdvice
public class ExceptionHandle {
public final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
@ExceptionHandler(value = Exception.class)
public ResponseEntity<Object> handleAssertionError(HttpServletRequest request, HandlerMethod method, Exception ex) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(String.format("%s/n%s", method.getShortLogMessage(), ex.getMessage()), ex);
}
// 构造返回的响应体
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("timestamp", LocalDateTime.now());
responseBody.put("error", "error.error");
responseBody.put("message", ex.getMessage());
responseBody.put("path", request.getRequestURI());
return new ResponseEntity<>(responseBody, HttpStatus.OK);
}
/**
* AccessDeniedException交由Security处理
*/
@ExceptionHandler(value = AccessDeniedException.class)
public void accessDeniedException(AccessDeniedException accessDeniedException){
throw accessDeniedException;
}
}
- 测试
注册功能完善
在注册时,可能会出现多个用户注册同一个登录名的情况,这种问题可以用分布式锁来解决
创建通用异常类
package com.learn.security.infra.exception;
import lombok.Getter;
/**
* @author PC
* 通用异常处理类
*/
@Getter
public class CommonException extends RuntimeException{
private final transient Object[] parameters;
private String code;
public CommonException(String code, Object... parameters) {
super(code);
this.parameters = parameters;
this.code = code;
}
public CommonException(String code, Throwable cause, Object... parameters) {
super(code, cause);
this.parameters = parameters;
this.code = code;
}
public CommonException(String code, Throwable cause) {
super(code, cause);
this.code = code;
this.parameters = new Object[0];
}
public CommonException(Throwable cause, Object... parameters) {
super(cause);
this.parameters = parameters;
}
public void setCode(String code) {
this.code = code;
}
}
注册功能加锁
- pom中添加redis相关依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson 依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
- application.yml中添加redis相关配置
spring:
redis:
host: 192.168.150.128
port: 6379
password: 123456
jedis:
pool:
# 资源池中最大连接数
# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整
max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
# 资源池运行最大空闲的连接数
# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销
max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
# 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
# 默认 -1 表示永不超时,设置5秒
max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
- 注册功能加锁
@Override
@Transactional
public RegisterDTO register(RegisterDTO registerDTO) {
boolean validFlag = !(Objects.isNull(registerDTO) || StringUtils.isEmpty(registerDTO.getLoginName()) || StringUtils.isEmpty(registerDTO.getPassword())
|| StringUtils.isEmpty(registerDTO.getVerifyPassword()));
//必要信息缺失
Assert.isTrue(validFlag, "error.necessary.information.missing");
//两次密码不一致
Assert.isTrue(StringUtils.equals(registerDTO.getPassword(), registerDTO.getVerifyPassword()), "error.auth_password.wrong");
//密码加密存储
try {
registerDTO.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
} catch (Exception exception) {
LOGGER.error("encode password:{} failed{}", registerDTO.getPassword(), exception.getMessage());
throw new CommonException("error.password.encode");
}
RLock rLock = redissonClient.getLock("lock:" + registerDTO.getLoginName());
try {
//获取锁等待3秒,10秒过期
if (rLock.tryLock(3, 10, TimeUnit.SECONDS)) {
//数据库中是否有这个loginName
if (Objects.isNull(this.getLoginInfo(registerDTO.getLoginName()))) {
Login newLogin = new Login();
BeanUtils.copyProperties(registerDTO, newLogin);
loginMapper.insert(newLogin);
} else {
LOGGER.error(String.format("error.loginName.exist:%s", registerDTO.getLoginName()));
throw new CommonException("error.loginName.exist");
}
}
} catch (Exception exception) {
throw new CommonException("error.register:" + exception.getMessage());
} finally {
// 释放锁
rLock.unlock();
}
registerDTO.setSuccessFlag(1);
return registerDTO;
}
测试
暂时将login表的唯一性索引改为普通索引,使用jmeter进行测试
- 数据库
- Jmeter
点击运行,查看数据库结果
记住我
当前程序当关闭浏览器后重新打开需要权限校验的页面,会跳回登录页,这个在某些场景是不合理的,因此还需要实现一个记住我的功能
创建自动登陆表
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
配置类添加自动注册的配置
private UserDetailsService userDetailsService;
@Lazy
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
//com.learn.security.config.SecurityAutoConfiguration#filterChain添加如下配置
//自动登录
.and().rememberMe().tokenRepository(persistentTokenRepository())
//自动登录超时时间
.tokenValiditySeconds(24*60*60)
//设置userDetailService
.userDetailsService(userDetailsService)
页面添加记住我
注意:name的属性值必须为remember-me,不能改为其他值
<div><label>
<input type="checkbox" name="remember-me">
</label>记住密码
</div>
测试
- 关闭浏览器,重新访问http://127.0.0.1:8888/v1/auth/role-admin
- 登出
参考资料
[1]gitee项目仓库地址
标签:功能完善,springframework,registerDTO,学习,org,import,security,SpringSecurity,public From: https://blog.csdn.net/weixin_43625238/article/details/140518703