本部分内容主要来源于鱼皮智能协图云图库部分,并在笔者个人项目学习的基础上进行扩展衍生。
用户模块通常需要解决以下功能:用户注册,用户登录,获取当前登录用户信息,用户退出登录,
用户权限控制,管理员管理用户。接下来我们逐个解决。
库表设计
sql
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
userName varchar(256) null comment '用户昵称',
userAvatar varchar(1024) null comment '用户头像',
userProfile varchar(512) null comment '用户简介',
userRole varchar(256) default 'user' not null comment '用户角色:user/admin',
editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
UNIQUE KEY uk_userAccount (userAccount),
INDEX idx_userName (userName)
) comment '用户' collate = utf8mb4_unicode_ci;
索引设计:常查询且有较高辨识度的字段使用索引
editTime 和 updateTime 的区别:editTime 表示用户编辑个人信息的时间(需要业务代码来更新),而 updateTime 表示这条用户记录任何字段发生修改的时间(由数据库自动更新)
良好开发习惯:将sql语句保存在项目sql/create_table.sql 文件中
用户登录流程与实现思路

整个登录校验大致分为两个模块:
-
会话跟踪技术:用户登录成功之后,在后续的每一次请求中,都可以获取到该标记。可以使用cookie,session,令牌,甚至redis实现。本质就是生成唯一id串联起整个会话过程。
-
统一拦截技术:过滤器Filter、拦截器Interceptor(黑马web项目,redis项目中的),Spring AOP 切面 + 自定义权限校验注解(鱼皮项目中的)
会话跟踪技术
Cookie(客户端会话跟踪技术):数据存储在客户端浏览器当中
用户信息存在浏览器中非常不安全,基本不用。
-
响应头 Set-Cookie :设置Cookie数据的
-
请求头 Cookie:携带Cookie数据的

java
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
Session(服务端会话跟踪技术):数据存储在储在服务端
这里的session技术更像是session技术+cookie技术。将sessionid自动 传递到cookie之中,浏览器前后端通过sessionid来确认同一会话,而用户信息在单体架构中存在HttpSession 对象中,在分布式架构中可通过redis,mysql等进行存储。
java
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
令牌技术
jwt令牌。第一部分:Header(头), 记录令牌类型、签名算法等。第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
对JSON格式的数据进行一次编码:进行Base64编码,Base64是编码方式,而不是加密方式。
如何返回jwt:json返回jwt(需要前端解析),存入Cookie
如何存储在浏览器中:
1.localStorage持久化存储
2.sessionStorage会话级存储
3.Cookie(HttpOnly) 存在浏览器 Cookie 中,设置 HttpOnly 后无法被 JS 读取
如何携带jwt:http请求头,cookie自动携带
生成jwt令牌流程:引依赖-jwt工具类-业务校验代码实现
java
package com.itheima.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey = "SVRIRUlNQQ==";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @return
*/
public static String generateJwt(Map<String,Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
统一拦截技术
过滤器与拦截器主要有以下区别
-
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
-
拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
过滤器Filter
-
第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
-
第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
java
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
//初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init ...");
}
//拦截到请求时,调用该方法,可以调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
System.out.println("拦截到了请求...");
}
//销毁方法, web服务器关闭时调用, 只调用一次
public void destroy() {
System.out.println("destroy ... ");
}
}
java
@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasManagementApplication.class, args);
}
}
Servlet 组件是 Java EE(Servlet 规范)定义的、由 Web 容器(如 Tomcat)负责初始化 / 执行 / 销毁的核心组件,不是 Spring 框架的组件。包括Servlet(请求处理器)、Filter(过滤器)、Listener(监听器)
Spring Boot 中写的 @Controller 接口,底层其实是 Spring MVC 封装了一个核心 Servlet------DispatcherServlet(前端控制器)
拦截器Interceptor
-
定义拦截器
-
注册配置拦截器
java
//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //true表示放行
}
//目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}
//视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
实际开发
会话跟踪
数据访问层的代码
一般包括实体类、MyBatis 的 Mapper 类和 XML 等,可通过 MyBatisX 代码生成插件。
数据模型开发
在MyBatis生成代码基础上修改:
- id 默认是连续生成的,容易被爬虫抓取,所以更换策略为
ASSIGN_ID雪花算法生成。 - 数据删除时默认为彻底删除记录,如果出现误删,将难以恢复,所以采用逻辑删除 ------ 通过修改 isDelete 字段为 1 表示已失效的数据。
枚举类
用于定义用户角色(admin,user)
用户注册,登录,退出,查询当前登录用户
1.每种请求都单独定义请求参数类
2.注册时将密码加密存储(加密算法+盐值)
3.登录后保存用户信息至session
4.查询当前登录用户采取数据脱敏
5.退出时删除当前session保存的信息即可(request.getSession().removeAttribute(USER_LOGIN_STATE);)
统一拦截/鉴权
这里采取Spring AOP 切面 + 自定义权限校验注解的方式
1.自定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
String mustRole() default "";
}
2.权限校验切面
java
@Aspect
@Component
public class AuthInterceptor {
@Resource
private UserService userService;
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
User loginUser = userService.getLoginUser(request);
UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustRoleEnum == null) {
return joinPoint.proceed();
}
UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());
if (userRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
if (UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
return joinPoint.proceed();
}
}
只要给方法添加了 @AuthCheck 注解,就必须要登录,否则会抛出异常。并且可以根据属性赋值来明确方法的权限范围。
完善增删改查
这里主要是分页查询有点难度,因此只说明这点。
1.每种请求都单独定义请求参数类,分页查询则指明查询页码,每页条数等信息。
- UserService 中编写方法,专门用于将查询请求转为 QueryWrapper 对象
java
@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
if (userQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
Long id = userQueryRequest.getId();
String userAccount = userQueryRequest.getUserAccount();
String userName = userQueryRequest.getUserName();
String userProfile = userQueryRequest.getUserProfile();
String userRole = userQueryRequest.getUserRole();
String sortField = userQueryRequest.getSortField();
String sortOrder = userQueryRequest.getSortOrder();
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(ObjUtil.isNotNull(id), "id", id);
queryWrapper.eq(StrUtil.isNotBlank(userRole), "userRole", userRole);
queryWrapper.like(StrUtil.isNotBlank(userAccount), "userAccount", userAccount);
queryWrapper.like(StrUtil.isNotBlank(userName), "userName", userName);
queryWrapper.like(StrUtil.isNotBlank(userProfile), "userProfile", userProfile);
queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);
return queryWrapper;
}
3.利用MyBatis-Plus实现分页查询
(1)引入依赖与插件---------样板代码
(2)Controller层进行查询------构建Page<T>对象
java
@PostMapping("/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest) {
ThrowUtils.throwIf(userQueryRequest == null, ErrorCode.PARAMS_ERROR);
long current = userQueryRequest.getCurrent();
long pageSize = userQueryRequest.getPageSize();
Page<User> userPage = userService.page(new Page<>(current, pageSize),
userService.getQueryWrapper(userQueryRequest));
Page<UserVO> userVOPage = new Page<>(current, pageSize, userPage.getTotal());
List<UserVO> userVOList = userService.getUserVOList(userPage.getRecords());
userVOPage.setRecords(userVOList);
return ResultUtils.success(userVOPage);
}
最后由于前端 JS 的精度范围有限,我们后端返回的 id 范围过大,导致前端精度丢失,会影响前端页面获取到的数据结果。需要数据精度修复。(样板代码)
至此一个带有增删改查功能与鉴权功能的管理系统就写完了。