本项目所将主要用到技术点:
前端:Vue2、ElementUI。
后端:Spring Boot、Spring Security、MySQL、MyBatis-Plus、Docker
本章目标为完成后端的用户登录逻辑已完成前期基础部分。
目录
前置工作
状态码枚举
/**
* 状态码枚举
*/
@Getter
public enum CodeEnum {
SUCCESS(200, "成功", "通用成功码"),
FAIL(-1, "失败", "通用失败码"),
AUTH_FAIL(401, "无权限", "无权限"),
SYSTEM_ERROR(99999, "系统异常", "系统异常")
;
private final int code;
private final String msg;
private final String desc;
CodeEnum(Integer code, String msg, String desc) {
this.code = code;
this.msg = msg;
this.desc = desc;
}
}
基础实体类
定义一个BaseEntity,定义类的通用字段,其他类继承这个基础实体类。
/**
* 基础实体类
*/
@Data
public class BaseEntity {
/**
* 主键id(指定使用雪花算法生成 ID)
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
private Integer delFlag;
}
统一接口返回结果类
在返回数据时,我们统一规定用自定义的Result类来返回结果。
/**
* 统一接口返回结果类
*/
@Data
public class Result<T> implements Serializable {
private int code;
private String message;
private T data;
public Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 通用成功返回方法
*/
public static <T> Result<T> success(T data) {
return new Result<>(CodeEnum.SUCCESS.getCode(), CodeEnum.SUCCESS.getMsg(), data);
}
/**
* 通用失败返回方法(无数据体)
*/
public static <T> Result<T> failure() {
return new Result<>(CodeEnum.FAIL.getCode(), CodeEnum.FAIL.getMsg(), null);
}
/**
* 通用失败返回方法(包含数据体)
*/
public static <T> Result<T> failure(T data) {
return new Result<>(CodeEnum.FAIL.getCode(), CodeEnum.FAIL.getMsg(), data);
}
/**
* 自定义失败返回方法(无数据体)
*/
public static <T> Result<T> failure(int code, String message) {
return new Result<>(code, message, null);
}
/**
* 自定义失败返回方法(包含数据体)
*/
public static <T> Result<T> failure(int code, String message, T data) {
return new Result<>(code, message, data);
}
/**
* 使用错误枚举返回失败(无数据体)
*/
public static <T> Result<T> failure(CodeEnum codeEnum) {
return new Result<>(codeEnum.getCode(), codeEnum.getMsg(), null);
}
/**
* 使用错误枚举返回失败(包含数据体)
*/
public static <T> Result<T> failure(CodeEnum codeEnum, T data) {
return new Result<>(codeEnum.getCode(), codeEnum.getMsg(), data);
}
}
myBatis-Plus部分功能配置
Snowflake雪花算法配置
Snowflake雪花算法是一种分布式唯一ID生成算法, 由Twitter 公司开源。在分布式项目中还是比较常见的,它能确保在多个节点上生成的ID都是唯一的。这里我们的项目生成数据库表数据,都由它来生成主键ID。
既然引入了MyBatisPlus,所以这里我们直接使用MyBatisPlus中自带的雪花算法生成功能就好。直接新建一个配置类进行基本的配置:
/**
* mybatis-plus配置类
*/
@Configuration
public class MyBatisPlusConfig {
private final long workerId = 1; // 终端ID
private final long datacenterId = 1; // 数据中心ID
/**
* 雪花算法生成器
*/
@Bean
public IdentifierGenerator idGenerator() {
return new IdentifierGenerator() {
Sequence sequence = new Sequence(workerId, datacenterId);
@Override
public Number nextId(Object entity) {
return sequence.nextId();
}
@Override
public String nextUUID(Object entity) {
return Long.toString(sequence.nextId());
}
};
}
}
全局delFlag=0条件拼接
由于项目中,所有表统一使用delFlag来标记该条数据是否删除,为了不用每次查询都手动加上"where del_flag=0"这样的条件,这里通过使用mybatis-plus所带的全局多租户拦截器实现这个功能。
首先创建一个自定义多租户处理器:
/**
* 自定义多租户处理器
*/
@Component
public class MyTenantLineHandler implements TenantLineHandler {
@Override
public Expression getTenantId() {
// 返回一个表达式,表示全局过滤条件,这里使用 delFlag=0
return new LongValue(0);
}
@Override
public String getTenantIdColumn() {
// 返回需要过滤的字段名称,这里是 delFlag
return "del_flag";
}
@Override
public boolean ignoreTable(String tableName) {
// 如果有些表不需要过滤,可以在这里指定
return false;
}
}
然后在上述的mybatis-plus的配置类中(MyBatisPlusConfig )添加:
/**
* mybatisPlus拦截器配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(MyTenantLineHandler tenantLineHandler) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加多租户拦截器,用于处理全局查询条件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));
return interceptor;
}
分页拦截器
在上述mybatisPlus的拦截器配置中添加拦截器:
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
公共字段自动填充配置
在数据增删改查时,自动处理创建、更新时间等公共字段
/**
* 配置创建、更新时间的自动填充
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
setFieldValByName("createTime", now, metaObject);
setFieldValByName("updateTime", now, metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
全局异常处理器
这里大概分为三类:运行时异常、非运行时异常和自定义异常。
运行时异常:即RuntimeException类及其子类异常,是项目运行时出现的异常。
非运行时异常:即RuntimeException以外的异常,该异常为编译阶段强制要求处理的异常。
自定义异常:用于一些需要特定处理的异常,如多模块项目中,每个模块自己的业务异常等。
首先,创建一个自定义异常:
/**
* 自定义异常
*/
@Getter
public class CustomException extends RuntimeException{
private final Integer code;
private final String msg;
private final String desc;
public CustomException(Integer code, String msg, String desc){
super(msg);
this.code = code;
this.msg = msg;
this.desc = desc;
}
public CustomException(CodeEnum codeEnum){
this(codeEnum.getCode(), codeEnum.getMsg(), codeEnum.getDesc());
}
public CustomException(CodeEnum codeEnum, Object template1, Object template2){
this(codeEnum.getCode(), StrUtil.format(codeEnum.getMsg(), template1), StrUtil.format(codeEnum.getDesc(), template2));
}
}
然后,创建全局异常处理器:
/**
* 全局异常处理器
*/
@Configuration
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger ERRORLOGGER = LoggerFactory.getLogger("errorLog");
@ExceptionHandler(RuntimeException.class)
public Result<?> unDefinedExceptionHandler(RuntimeException e , HttpServletResponse response){
ERRORLOGGER.error("运行时异常 ", e);
return Result.failure(CodeEnum.SYSTEM_ERROR.getCode(),CodeEnum.SYSTEM_ERROR.getMsg());
}
@ExceptionHandler(value = Exception.class)
public Result<String> exceptionHandler(Exception e ,HttpServletResponse response){
ERRORLOGGER.error("非运行时异常 ",e);
return Result.failure(CodeEnum.SYSTEM_ERROR.getCode(),CodeEnum.SYSTEM_ERROR.getMsg());
}
@ExceptionHandler(CustomException.class)
public Result<String> customException(CustomException e){
ERRORLOGGER.failure("自定义异常, code: {}, msg: {}, desc: {}",
e.getCode(), e.getMsg(),e.getDesc());
return Result.failure(e.getCode(), e.getMsg());
}
}
创建用户表
创建一张用户表,并分别创建它对应的实体类、mapper、service、serviceImpl层,为加快效率,我们直接使用IDEA中的MybatisX插件来生成。
建表:
使用MybatisX生成各业务逻辑层类:
用户增删改查基本逻辑
以上生成完毕后,分别对User、UserService、UserServiceImpl类作修改、并创建Controller层:
修改User类
继承BaseEntity,修改后如下:
@TableName(value ="user")
@Data
public class User extends BaseEntity implements Serializable {
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
/**
* 是否启用
*/
private Integer enabled;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
UserService类
public interface UserService extends IService {
/**
* 创建单条数据
*/
int createOne(User user);
/**
* 创建单条数据
*/
int updateOne(User user);
/**
* 删除单条数据
*/
int deleteOne(Long id);
/**
* 批量删除
*/
int deleteBatch(List<Long> idList);
/**
* 查询多条数据
*/
IPage<User> findList(UserRequestDto userRequestDto);
/**
* 单条查询
*/
User findOne(Long id);
}
UserServiceImpl类
定义基本的增删改查接口:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Resource
public UserMapper userMapper;
@Resource
private PasswordEncoder passwordEncoder;
/**
* 创建单条数据
* @param user 用户实体类参数
* @return int
*/
@Override
public int createOne(User user) {
Assert.notNull(user, "用户实体类参数不能为空");
Assert.hasText(user.getPassword(), "密码不能为空");
user.setEnabled(1);
//密码加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userMapper.insert(user);
}
/**
* 修改单条数据
* @param user 用户实体类参数
* @return int
*/
@Override
public int updateOne(User user) {
Assert.notNull(user, "用户实体类参数不能为空");
Assert.notNull(user.getId(), "用户主键ID不能为空");
if(user.getPassword() != null){
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
return userMapper.updateById(user);
}
/**
* 删除单条数据
* @param id 主键
* @return int
*/
@Override
public int deleteOne(Long id) {
Assert.notNull(id, "主键id不能为空");
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
//根据id指定单条数据
updateWrapper.lambda().eq(User::getId,id);
//将 删除标记 置为1
updateWrapper.lambda().set(User::getDelFlag,1);
return userMapper.update(updateWrapper);
}
/**
* 批量删除
* @param idList 主键list
* @return int
*/
public int deleteBatch(List<Long> idList){
Assert.notEmpty(idList, "主键id的list集合不能为空");
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
//根据idList指定若干条数据
updateWrapper.lambda().in(User::getId,idList);
//将 删除标记 置为1
updateWrapper.lambda().set(User::getDelFlag,1);
return userMapper.update(updateWrapper);
}
/**
* 查询多条数据
* @param userRequestDto 用户请求实体类参数
* @return IPage<User>
*/
@Override
public IPage<User> findList(UserRequestDto userRequestDto) {
Assert.notNull(userRequestDto, "用户请求实体类参数不能为空");
//分页参数
Page<User> page = new Page<>(
userRequestDto.getPageNum()==null?1:userRequestDto.getPageNum(),
userRequestDto.getPageSize()==null?10:userRequestDto.getPageSize()
);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
return userMapper.selectPage(page,queryWrapper);
}
/**
* 单条查询
* @param id 主键
* @return User
*/
@Override
public User findOne(Long id) {
Assert.notNull(id, "主键id不能为空");
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getId,id);
return userMapper.selectOne(queryWrapper);
}
}
创建UserController层
@RestController
@RequestMapping("/users")
public class UserController {
@Resource
public UserService userService;
/**
* 创建一条数据
* @param user 用户实体类参数
* @return 统一接口返回结果类
*/
@PostMapping("/createOne")
public Result<?> createOne(@RequestBody User user){
return Result.success(userService.createOne(user));
}
/**
* 修改一条数据
* @param user 用户实体类参数
* @return 统一接口返回结果类
*/
@PostMapping("/updateOne")
public Result<?> updateOne(@RequestBody User user){
return Result.success(userService.updateOne(user));
}
/**
* 删除一条数据
* @param id 主键id
* @return 统一接口返回结果类
*/
@PostMapping("/deleteOne/{id}")
public Result<?> deleteOne(@PathVariable Long id){
return Result.success(userService.deleteOne(id));
}
/**
* 批量删除
* @param idList 主键id集合
* @return 统一接口返回结果类
*/
@PostMapping("/deleteBatch")
public Result<?> deleteBatch(@RequestBody List<Long> idList){
return Result.success(userService.deleteBatch(idList));
}
/**
* 分页查询多条数据
* @param userRequestDto 用户请求实体类参数
* @return 统一接口返回结果类
*/
@PostMapping("/findList")
public Result<?> findList(@RequestBody UserRequestDto userRequestDto){
return Result.success(userService.findList(userRequestDto));
}
/**
* 查询一条数据
* @param id 主键id
* @return 统一接口返回结果类
*/
@GetMapping("/findOne/{id}")
public Result<?> findOne(@PathVariable Long id){
return Result.success(userService.findOne(id));
}
}
配置Spring Security
创建配置类SecurityConfig
@EnableMethodSecurity //开启方法授权
@Configuration
public class SecurityConfig {
@Resource
private DBUserDetailsManager userDetailsManager;
@Bean
public PasswordEncoder passwordEncoder() {
//默认编码器为BCrypt
String defaultEncode = "bcrypt";
Map<String, PasswordEncoder> encoderMap = new HashMap<>();
// 添加BCrypt编码器,并自定义工作因子
encoderMap.put(defaultEncode, new BCryptPasswordEncoder(4));
// 添加NoOp编码器
encoderMap.put("noop", NoOpPasswordEncoder.getInstance());
return new DelegatingPasswordEncoder(defaultEncode, encoderMap);
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.
getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsManager)
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginProcessingUrl("/myLogin")
.successHandler(new MyAuthenticationSuccessHandler()) //登录成功后处理
.failureHandler(new MyAuthenticationFailureHandler()) //登录失败后处理
.permitAll()
)
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用 CORS 配置
.csrf(AbstractHttpConfigurer::disable) // 关闭 CSRF 保护
.authorizeHttpRequests(auth -> auth
.requestMatchers("/myLogin", "/logout").permitAll() // 允许未认证的访问
.anyRequest().authenticated() // 其他请求需要认证
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 处理未认证的请求
)
.logout(logout -> logout
.logoutUrl("/api/logout") // 注销处理 URL
.logoutSuccessHandler(new MyLogoutSuccessHandler()) // 注销成功后的处理
.permitAll()
)
;
return http.build();
}
/**
* 跨域资源访问配置
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// *表示允许任何来源的请求访问,一般改成项目所属的前端域名或ip端口号
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
登录相关处理器实现
分别实现AuthenticationSuccessHandler(认证成功处理器)、AuthenticationSuccessHandler(认证失败处理器)、AuthenticationEntryPoint(认证入口点处理器)、LogoutSuccessHandler(注销成功处理器)、SessionInformationExpiredStrategy(会话并发处理器):
/**
* 认证成功后处理器
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//获取用户身份信息
Object principal = authentication.getPrincipal();
//获取用户权限信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("principal", principal);
userInfo.put("authorities", authorities);
Result<Object> successResult = Result.success(userInfo);
String jsonResult = JSON.toJSONString(successResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(jsonResult);
}
}
/**
* 认证失败处理器
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//获取异常信息
String localizedMessage = exception.getLocalizedMessage();
HashMap result = new HashMap();
result.put("data",localizedMessage);
result.put("extra","认证失败");
//将异常信息放入自定义result
Result<Object> failureResult = Result.failure(result);
//返回结果转为json字符串
String jsonResult = JSON.toJSONString(failureResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(jsonResult);
}
}
/**
* 认证入口点处理器
* 用于定义当用户尝试访问受保护的资源但没有进行身份验证时应该执行的操作
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//获取异常信息
String localizedMessage = authException.getLocalizedMessage();
HashMap result = new HashMap();
result.put("data",localizedMessage);
result.put("extra","无权限");
//将异常信息放入自定义result
Result<Object> failureResult = Result.failure(result);
//返回结果转为json字符串
String jsonResult = JSON.toJSONString(failureResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(jsonResult);
}
}
/**
* 注销成功处理器
*/
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Result<Object> successResult = Result.success("注销成功");
String jsonResult = JSON.toJSONString(successResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(jsonResult);
}
}
/**
* 会话并发处理器(同一账号后登陆的会使先登录的退出登录)
*/
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
Result<Object> result = Result.success("该账号已于另一程序登录");
String resultJson = JSON.toJSONString(result);
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(resultJson);
}
}
实现UserDetailsManager接口(用户身份验证管理类)
新建类DBUserDetailsManager实现UserDetailsManager接口。
在这个类中,定义从数据库中获取用户信息、获取用户权限等操作逻辑。
/**
* 用户身份验证管理类
*/
@Component
public class DBUserDetailsManager implements UserDetailsService {
@Resource
private UserMapper userMapper;
/**
* 从数据库中获取用户信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User user = userMapper.selectOne(userQueryWrapper);
if(Objects.isNull(user)){
throw new UsernameNotFoundException("用户 " + username + " 未找到");
}else{
// 获取用户角色和权限
List<GrantedAuthority> authorities = getUserAuthorities(user);
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.disabled("1".equals(user.getEnabled()))
.credentialsExpired(false)
.accountLocked(false)
.authorities(authorities)
.build();
}
}
/**
* 获取用户权限
* @param user 用户实体
* @return 用户权限集合
*/
private List<GrantedAuthority> getUserAuthorities(User user) {
// 创建一个空的权限列表
List<GrantedAuthority> authorities = new ArrayList<>();
// 添加用户的权限(暂不从数据库获取)
authorities.add(new SimpleGrantedAuthority("ADMIN"));
return authorities;
}
}
基本登录逻辑测试
以上内容完成后,登录的简单后端逻辑基础已完成,现在开始测试登录、注销和增删改查等接口是否能正常运行。
为了便于测试,这里会随便在user表中填入一些数据。
其中只有一条数据是有用的,为管理员账号:
账号:root 密码:{bcrypt}$2a
04
04
04F8t3XoRWVzB2dUgKDWtSg./ZjRJ7bXt0utZC4XfBvkPz2zJ9j9YT2
ps:密码是123的bcrypt加密后数据
登录
增删改查
注销
小结
以上工作完成后,后端的用户登录逻辑已完成前期基础部分,后续章节进一步完善。