所用的技术
- springboot
- jwt
- maven
- mysql
- mybatis
- knife4j
- springdata jpa
准备工作
maven依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.22</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.20</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>3.3.2</version>
</dependency>
数据库
CREATE DATABASE IF NOT EXISTS database_name
CREATE TABLE `user` (
`id` bigint(11) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`username` varchar(255) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`status` int(11) DEFAULT NULL COMMENT '0:禁用 1:启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_by` int(11) DEFAULT NULL COMMENT '创建人id',
`update_by` int(11) DEFAULT NULL COMMENT '修改人id',
`del_flag` int(11) DEFAULT NULL COMMENT '0:删除 1:未删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息';
yml配置
application.yml
server:
port: 8080
spring:
profiles:
active: dev
main:
allow-circular-references: true
datasource:
druid:
driver-class-name: ${sky.datasource.driver-class-name}
url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: ${sky.datasource.username}
password: ${sky.datasource.password}
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
logging:
level:
com:
sky:
mapper: debug
service: info
controller: info
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: ithema
user-ttl: 7200000
user-token-name: authentication
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}
mchid: ${sky.wechat.mchid}
mchSerialNo: ${sky.wechat.mchSerialNo}
privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
apiV3Key: ${sky.wechat.apiV3Key}
weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
notifyUrl: ${sky.wechat.notifyUrl}
refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
application-dev.yml
wzy:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: database_name
username: 账号
password: 密码
实体类
entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user")
public class User {
// 用户唯一标识
@Id
private Long id;
// 姓名
private String name;
// 用户名
private String username;
// 密码
private String password;
// 状态: 0:禁用 1:启用
private Integer status;
// 创建时间
private LocalDateTime createTime;
// 更新时间
private LocalDateTime updateTime;
// 创建人
private Integer createBy;
// 更新人
private Integer updateBy;
// 删除标识: 0:未删除 1:已删除
private Integer delFlag;
}
dto
后端接受前端传来的信息
@Data
@ApiModel(description = "用户登录信息")
public class UserLoginDTO implements Serializable {
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
}
vo
后端传给前端的信息
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "用户登录返回信息")
public class UserLoginVO implements Serializable {
@ApiModelProperty("主键值")
private Long id;
@ApiModelProperty("jwt令牌")
private String token;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("用户名")
private String username;
}
result 数据返回类
作一个统一的返回值
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
业务异常处理
给出现所有不合法的操作一个统一的处理,这里只列举的一部分,如果给还需其他异常处理,请自行添加
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String message) {
super(message);
}
}
密码错误异常处理
public class PasswordErrorException extends BaseException {
public PasswordErrorException() {}
public PasswordErrorException(String message) {
super(message);
}
}
用户不存在异常处理
public class UserNotFoundException extends BaseException{
public UserNotFoundException() {}
public UserNotFoundException(String message)
{
super(message);
}
}
用户被禁用异常
public class UserStopException extends BaseException{
public UserStopException() {}
public UserStopException(String message) {
super(message);
}
}
全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获全局异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex) {
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
消息提示类常量
将其作为类封装,便于后期的维护
public class MessageConstant {
public static final String USER_NOT_FOUND = "用户不存在";
public static final String USER_NOT_ENABLE = "用户未启用";
public static final String USER_NOT_LOGIN = "用户未登录";
public static final String UNKNOWN_ERROR = "未知错误";
public static final String USER_LOGIN_SUCCESS = "用户登录成功";
public static final String LOGIN_FAILED = "登录失败";
public static final String PASSWORD_ERROR = "密码错误";
}
Knife4j配置
kniife4j:目前主流的接口显示文档
@Configuration
public class Knife4jConfig {//对于配置类要求可以看懂即可,不用反复去写,将来可以CV
@Bean
public OpenAPI springShopOpenApi() {
return new OpenAPI()
// 接口文档标题
.info(new Info().title("demo")
// 接口文档简介
.description("demo_demo")
// 接口文档版本
.version("1.0.0")
// 开发者联系方式
.contact(new Contact().name("顾随")
.email("****")));
}
}
使用threadLocal存储用户id
使用线程池存储用户id,便于后期数据使用
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
jwt
原理
- 前端发请求,将有账号和密码发给后端
- 后端核对前端发来的账号和密码,如果正确,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串。如果错误,则会之间报异常,封装统一result后,返回给前端。
- 后端将一个用户数据和生成的token,返回给前端
- 前端每次请求,都要将后端传来的token放入http请求的Authorization属性中
- 后端接受前端请求时,先检查前端传来的token,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
- 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
生成jwt相关配置
JwtProperties接受xml配置中的参数,放入其属性中
@Component
@ConfigurationProperties(prefix = "wzy.jwt")
@Data
public class JwtProperties {
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
jwt工具类
用于生成jwt和解析jwt
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
jwt检验拦截器
用户校验前端传来的token
@RequiredArgsConstructor
@Slf4j
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {
private final JwtProperties jwtProperties;
/**
* 拦截请求进行jwt令牌认证
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
// 不是Controller的方法直接通过
return true;
}
// 从请求头中获取token
String token = request.getHeader(jwtProperties.getUserTokenName());
// 校验token
try {
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(),token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
BaseContext.setCurrentId(userId);
// 通过,放行
return true;
} catch (Exception e) {
// 不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
业务编写
controller
@RestController
@RequestMapping("/user")
@Slf4j
@Tag(name = "用户登录接口")
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final JwtProperties jwtProperties;
@Operation(summary = "用户登录")
@PostMapping("/login")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDto)
{
log.info("用户登录:{}",userLoginDto);
User user = loginService.login(userLoginDto);
log.info("登录用户:{}",user);
// 登录成功后生成token
Map<String,Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
String token = JwtUtil.createJWT(
jwtProperties.getUserSecretKey(),
jwtProperties.getUserTtl(),
claims
);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.name(user.getName())
.token(token)
.username(user.getUsername())
.build();
return Result.success(userLoginVO);
}
}
service
public interface LoginService {
/**
* 用户登录
* @param userLoginDto
* @return
*/
User login(UserLoginDTO userLoginDto);
}
serviceImpl
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {
// private final UserRepository userRepository;
private final UserMapper userMapper;
/**
* 用户登录
* @param userLoginDto
* @return
*/
@Override
public User login(UserLoginDTO userLoginDto) {
String username = userLoginDto.getUsername();
String password = userLoginDto.getPassword();
User user = userMapper.findByUsername(username, DelFlagConstant.ENABLE);
// 处理各种异常
if (user == null) {
// 用户不存在
throw new UserNotFoundException(MessageConstant.USER_NOT_FOUND);
}
//密码比对
//对前端传来的明文密码进行加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(user.getPassword())) {
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (user.getStatus() == StatusConstant.STOP) {
throw new UserStopException(MessageConstant.USER_NOT_ENABLE);
}
return user;
}
}
Mapper
@Mapper
public interface UserMapper {
@Select("select * from user where username = #{username} and del_flag = #{delFlag}")
User findByUsername(String username, Integer delFlag);
}
运行
打开http://localhost:8090/doc.html,查看knife4j生成的接口文档
出现图中内容,表示knife4j部署成功
测试接口
登录成功
输入正确的账号,密码后,会出现token以及用户信息
密码错误情况
返回值中会显示“密码错误”
用户账号错误情况
返回值会出现:“用户不存在”
出现以上情况,表示后端登录配置成功