1、简介
springboot登录模板是一个标准的springboot登录模板样本,采用MVC三层架构、restful风格、mybatis框架、登录认证使用到了JWT验证,用户获取使用到了ThreadLocal;加密算法采用md5加密。
2、环境搭建
2.1、创建数据库
-- 创建数据库
create database big_event;
-- 使用数据库
use big_event;
-- 用户表
create table user (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) comment '密码',
nickname varchar(10) default '' comment '昵称',
email varchar(128) default '' comment '邮箱',
user_pic varchar(128) default '' comment '头像',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '用户表';
-- 分类表
create table category(
id int unsigned primary key auto_increment comment 'ID',
category_name varchar(32) not null comment '分类名称',
category_alias varchar(32) not null comment '分类别名',
create_user int unsigned not null comment '创建人ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间',
constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
);
-- 文章表
create table article(
id int unsigned primary key auto_increment comment 'ID',
title varchar(30) not null comment '文章标题',
content varchar(10000) not null comment '文章内容',
cover_img varchar(128) not null comment '文章封面',
state varchar(3) default '草稿' comment '文章状态: 只能是[已发布] 或者 [草稿]',
category_id int unsigned comment '文章分类ID',
create_user int unsigned not null comment '创建人ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间',
constraint fk_article_category foreign key (category_id) references category(id),-- 外键约束
constraint fk_article_user foreign key (create_user) references user(id) -- 外键约束
)
2.2、创建springboot工程,添加相应依赖到pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.3</version>
</parent>
<groupId>com.itheima</groupId>
<artifactId>big-event</artifactId>
<version>1.0-SNAPSHOT</version>
<name>big-event</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--java-jwt坐标-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!--单元测试的坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.3</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
2.3、配置application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/big_event
username: root
password: 123456
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8080
2.4、创建包结构,并准备实体类
2.4.1、包结构
2.4.2、实体类
user类
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Integer id;//主键ID
private String username;//用户名
private String password;//密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
Article类
import java.time.LocalDateTime;
public class Article {
private Integer id;//主键ID
private String title;//文章标题
private String content;//文章内容
private String coverImg;//封面图像
private String state;//发布状态 已发布|草稿
private Integer categoryId;//文章分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
Result类:主要用来返回结果
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
}
3、功能开发
3.1功能实现
功能列表:
1、注册
2、登录
3、获取用户详细信息
4、更新用户基本信息
4、更新用户头像
5、更新用户密码
原型图:
controller:
import com.itheima.pojo.Result;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import com.itheima.utils.JwtUtil;
import com.itheima.utils.Md5Util;
import com.itheima.utils.ThreadLocalUtil;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.constraints.URL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//查询用户
User u = userService.findByUserName(username);
if (u == null) {
//没有占用
//注册
userService.register(username, password);
return Result.success();
} else {
//占用
return Result.error("用户名已被占用");
}
}
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if (loginUser == null) {
return Result.error("用户名错误");
}
//判断密码是否正确 loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
return Result.error("密码错误");
}
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/) {
// //根据用户名查询用户
// Map<String, Object> map = JwtUtil.parseToken(token);
Map<String, Object> map= ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.findByUserName(username);
return Result.success(user);
}
@PutMapping("/update")
public Result update(@RequestBody @Validated User user) {
userService.update(user);
return Result.success();
}
@PatchMapping("updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl) {
userService.updateAvatar(avatarUrl);
return Result.success();
}
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String, String> params,@RequestHeader("Authorization") String token) {
//1.校验参数
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String rePwd = params.get("re_pwd");
if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) {
return Result.error("缺少必要的参数");
}
//原密码是否正确
//调用userService根据用户名拿到原密码,再和old_pwd比对
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User loginUser = userService.findByUserName(username);
if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
return Result.error("原密码填写不正确");
}
//newPwd和rePwd是否一样
if (!rePwd.equals(newPwd)){
return Result.error("两次填写的新密码不一样");
}
//2.调用service完成密码更新
userService.updatePwd(newPwd);
return Result.success();
}
}
service:
import com.itheima.pojo.User;
public interface UserService {
//根据用户名查询用户
User findByUserName(String username);
//注册
void register(String username, String password);
//更新
void update(User user);
//更新头像
void updateAvatar(String avatarUrl);
//更新密码
void updatePwd(String newPwd);
}
Impl:
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import com.itheima.utils.Md5Util;
import com.itheima.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUserName(String username) {
User u = userMapper.findByUserName(username);
return u;
}
@Override
public void register(String username, String password) {
//加密
String md5String = Md5Util.getMD5String(password);
//添加
userMapper.add(username,md5String);
}
@Override
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}
@Override
public void updateAvatar(String avatarUrl) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updateAvatar(avatarUrl,id);
}
@Override
public void updatePwd(String newPwd) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updatePwd(Md5Util.getMD5String(newPwd),id);
}
}
mapper:
import com.itheima.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface UserMapper {
//根据用户名查询用户
@Select("select * from user where username=#{username}")
User findByUserName(String username);
//添加
@Insert("insert into user(username,password,create_time,update_time)" +
" values(#{username},#{password},now(),now())")
void add(String username, String password);
//更新
@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}")
void update(User user);
//更新照片
@Update("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}")
void updateAvatar(String avatarUrl, Integer id);
//更新密码
@Update("update user set password=#{md5String},update_time=now() where id=#{id}")
void updatePwd(String md5String, Integer id);
}
3.2JWT认证
3.2.1JWT概述
jwt是一种登录认证方式,目的是防止用户在未登录的情况下,访问到其他资源。
没有登录认证情况下,后端资源会被随意访问:
加入JWT认证后,访问后端资源,会首先校验是否先登录
jwt结构:
1、Header(头), 记录令牌类型和签名算法等
2、PayLoad(载荷),携带自定义的信息
3、Signature(签名),对头部和载荷进行加密计算得来
3.2.2JWT使用
引入JWT依赖
<!--java-jwt坐标-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
工具包:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "itheima";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
test
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTest {
@Test
public void testGen() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "张三");
//生成jwt的代码
String token = JWT.create()
.withClaim("user", claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60*12))//添加过期时间
.sign(Algorithm.HMAC256("itheima"));//指定算法,配置秘钥
System.out.println(token);
}
@Test
public void testParse() {
//定义字符串,模拟用户传递过来的token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MDU2NzM0Nzl9" +
".sX_yz683OeobXddjKYc0eyiJJYp4QPSyDGvWYX22jQM";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims.get("user"));
//如果篡改了头部和载荷部分的数据,那么验证失败
//如果秘钥改了,验证失败
//token过期
}
}
拦截器LoginInterceptor类:
import com.itheima.utils.JwtUtil;
import com.itheima.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
拦截器WebConfig类:
import com.itheima.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}
3.3ThreadLocal
3.3.1 概述
作用:用于存放用户信息。
用来存取数据: set()/get()
使用ThreadLocal存储的数据, 线程安全
用完记得调用remove方法释放
3.3.2使用
工具包:
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
pu
在拦截器LoginInterceptor类存储用户信息:
在controller中获取用户信息