JavaWeb前后端分离网站从0开发(二)用户登录功能(1)

本项目所将主要用到技术点:
前端: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加密后数据

登录

在这里插入图片描述

增删改查

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

注销

在这里插入图片描述

小结

以上工作完成后,后端的用户登录逻辑已完成前期基础部分,后续章节进一步完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值