目录
一、环境搭建
1、数据库
2、引入依赖
3、配置信息
4、创建包结构和数据库实体类
二、接口开发-注册接口
前提准备
响应数据
需求分析
全局异常处理
代码编写
测试
三、接口开发-登录接口
前提准备
响应数据
需求分析
代码编写
测试
拦截器
测试
-- 创建数据库
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) -- 外键约束
)
org.mybatis.spring.boot
mybatis-spring-boot-starter
3.0.2
com.mysql
mysql-connector-j
runtime
org.projectlombok
lombok
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Drive
url: jdbc:mysql://localhost:3306/big_event
username: root
password: 123456
server:
port: 8888
先看user实体类,我们之所以没有为字段设置getter和setter就是因为我们可以为了代码的美观与简洁而使用lombok工具
lombok:在编译阶段,为实体类自动生成getter、setter、toString
使用方法就是再类上加上注解@Data
在实际的项目开发中,不会像之前写的代码一样,直接return回去一个字符串或者数据。而是有统一的响应结果的,也就是Result类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static Result 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);
}
}
@NoArgsConstructor:自动创建无参构造器
@AllArgsConstructor:自动创建有参构造器
注册功能,首先就要去确认用户名是否被占用,之后才能注册。其次既然是提交注册表单请求,那么一般就属于POST请求,而查询等业务才是GET请求。
既然要注册用户,那么就不能原封不动的将密码加入数据库,一定要对数据进行加密,这样才安全。我们这里使用MD5加密,可以直接使用引入工具类也可以选择引入依赖。这里就使用MD5工具类了。
除次之外还要对参数进行校验,比如要求是密码要是5~16位非空字符,那么就要先检查是否符合要求,其次才能添加进数据库。
Spring提供了一个参数校验框架,使用预定义的注解完成参数校验,叫做Spring Validation。
MD5加密与SpringValidation框架的使用方法https://blog.csdn.net/m0_56308072/article/details/131101062?spm=1001.2014.3001.5501
这个的目的就是因为当我们测试失败时,响应得到的接过并不符合result风格,也不美观。
我们更希望即使报这样的错误也要符合result类,因此我们可以定义一个全局类
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
//有的异常可能没有message,所以要先用三元运算符判断一下
return Result.error(StringUtils
.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
}
}
这样就可以捕获全局的异常了。
Controller层
@Validated
@RestController
@RequestMapping("/user")
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 user = userService.findByUserName(username);
if (user == null){
//注册
userService.register(username,password);
return Result.success();
}else {
//占用
return Result.error("用户名以及被占用");
}
}
}
Service层
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUserName(String name) {
return userMapper.findByUserName(name);
}
@Override
public void register(String username, String password) {
//加密
String md5Password = Md5Util.getMD5String(password);
//添加
userMapper.register(username,md5Password);
}
}
Mapper层
@Mapper
public interface UserMapper {
@Select("select * from user where username = #{name}")
User findByUserName(String name);
@Insert("insert into user(username,password,create_time,update_time)" +
"values(#{username},#{md5Password},now(),now())")
void register(String username, String md5Password);
}
既然是开发登录接口,那么最多的就是使用JWT令牌进行登录
什么是JWT?如何使用https://blog.csdn.net/m0_56308072/article/details/131144785?spm=1001.2014.3001.5501
导入依赖
com.auth0
java-jwt
4.4.0
为什么要使用jwt令牌,简单来说就是因为,我们其实是可以直接跳过登录验证/login,直接进入/list去访问数据。这当然是不符合安全的。
因此才需要一块jwt令牌,只有通过/login才能可以拿到令牌,然后访问/list的时候只有持有令牌才可以进行访问
令牌就是一段字符串,作用是承载业务数据,减少后续请求查询数据库的次数。
防止篡改,保证信息的合法性和有效性
用户登陆后,系统会自动下发JWT令牌,然后在后续的请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为Authorization,值为登陆时下发的JWT令牌。就是jwt携带在请求头中,我们在获取验证的时候就要hetHeader得到token然后解析验证。
如果检测到用户未登录,则http响应状态码为401。那么就需要一个responese对象(HttpServletResponse),它可以更改状态码
首先就要根据输入的用户名进行查询,判断用户是否存在,然后再查询密码进行登录验证
JwtUtil
public class JwtUtil {
private static final String KEY = "wal";
//接收业务数据,生成token并返回
public static String genToken(Map 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 parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
Controller层
登录成功,生成token
@PostMapping("/login")
public Result 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())){
//登录成功,生成Token
Map 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("密码错误");
}
访问其他接口,验证token
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/list")
public Result list(@RequestHeader(name = "Authorization") String token,
HttpServletResponse response){
//验证token
try {
Map claim = JwtUtil.parseToken(token);
return Result.success("访问成功");
} catch (Exception e) {
//未登录
response.setStatus(401);
throw new RuntimeException(e);
}
}
}
尝试跳过登录验证直接访问数据接口
正常登录,生成密钥
携带密钥访问数据接口
注:如果觉得每次都复制粘贴返回的token用来测试太麻烦,可以直接在这里设置一个全局的token,这样就不用每次测试接口的时候都在header里面粘贴上去token了。当然,改完记得save一下tab,不然不会生效。
但是注意,这里配置完全局token之后就不要再单独携带token,这样就会携带两个token过去,并报错
com.auth0.jwt.exceptions.JWTDecodeException: The token was expected to have 3 parts, but got > 3.
向上面的例子中,我们只有一个数据访问接口的时候体现不出来,但是假如我们现在已经写了很多个接口,如果每个接口中都写验证token的逻辑未免也太繁琐了。我们希望这种复用性强的代码只写一次就好了,然后我们可以再不惊动原代码的情况下对功能进行添加,也就是AOP的思想。在这里应用的技术就是拦截器。
使用拦截器,首先要创建一个拦截器类然后集成HandlerInterceptor接口。其中重要的是preHandle方法
preHandle方法:在目标方法执行前执行,也就是在你访问接口的时候就直接拦截下来。那么拦截下来之后就要验证token了。
我们之前是通过参数声明的方式直接拿到,但是在重写的这个方法中并没有。这是因为他直接被包含在了参数request中(HttpServletRequest),这个对象顾名思义,就是代表请求,所有的请求数据都在request对象中;反之,与其相对的是参数response(HttpServletRequest)代表响应,所有的响应数据都在response数据中。
@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 claims = JwtUtil.parseToken(token);
//没有异常就放行
return true;
} catch (Exception e) {
//未登录,不放行
response.setStatus(401);
return false;
}
}
}
此时虽然完成了拦截器,但是他还没有生效,需要先把他注册之后才会生效,保证安全。在拦截器上加上@Component注解那么就代表交给了IOC容器管理变成了一个bean对象。
那么我们就在config创建一个拦截器的config表示启用哪些拦截器,重写其中的addIntercepter方法。顾名思义,就是添加那些拦截器。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//放行登录接口和注册接口
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/user/login","/user/register");
}
}
注意:既然是拦截器,那么它就能拦截所有的接口。一定要注意不能让它拦截登录接口和注册接口,不然要访问数据接口就要登录生成的token,要登陆的token就要访问登录接口,由于登录接口也被拦截了,那么访问登录接口就要登录生成的token,要登录的token就要访问登录接口......开启了无限套娃。addInterceptor就是添加要使用的拦截器,excludePathPatterns就是要排除拦截的接口.
当我们不携带令牌时,无响应,且状态码为401
携带令牌时成功访问