1.注册流程
2.数据库表的设计
分析:
假设一个用户只有一个角色,但一个角色可以有多个用户,则用户与角色之间是多对一的关系
一个角色可以有多个权限,一个权限可以有多个角色,角色与权限之间是多对多的关系
综上:可以创建四张表,用户表、角色表、权限表以及角色和权限之间关系的中间表
3.创建视图
查询用户的权限需要进行四表连接,因此做一个视图表便于查询,(可以把四表连接查询的select语句放入视图创建工具中,会自动生成好创建视图的语句)
4.创建maven项目,在pom文件中导入需要的包
<?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>
<groupId>com.wnhz.auth</groupId>
<artifactId>springboot-privs-token</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.druid.version>1.2.18</project.druid.version>
<project.mybatis.plus.version>3.5.3.1</project.mybatis.plus.version>
<project.knife4j.version>4.1.0</project.knife4j.version>
</properties>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.14</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${project.druid.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${project.mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>${project.knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.22.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
</dependencies>
</project>
5.配置yml文件
server:
port: 10002
spring:
mvc:
format:
date: yyyy-MM-dd
jackson:
date-format: yyyy-MM-dd
application:
name: privs-token
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.21.129:3306/bookdata?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123
redis:
database: 0
host: 192.168.21.129
port: 6379
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
knife4j:
enable: true
#项目日志
logging:
level:
com.wnhz.auth: debug
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
6.创建实体类
package com.wnhz.auth.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user_privs_view")
public class UserPrivs {
@TableField("user_username")
private String username;
@TableField("user_password")
private String password;
@TableField("privs_name")
private String privsname;
}
7.实现dao层接口
package com.wnhz.auth.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wnhz.auth.entity.UserPrivs;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IUserPrivsDao extends BaseMapper<UserPrivs> {
}
8.创建dto的结果返回类
package com.wnhz.auth.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
@ApiModel("DTO返回数据")
@Getter
@ToString
@Setter
public class HttpResp<T> implements Serializable {
private ResultCode resultCode;
@ApiModelProperty("测试时间")
@JsonFormat(timezone = "GMT+8")
private Date time;
@ApiModelProperty("测试结果")
private T results;
private HttpResp(){}
public static <T> HttpResp <T> result(ResultCode resultCode,Date time,T results){
HttpResp httpResp = new HttpResp();
httpResp.setResultCode(resultCode);
httpResp.setTime(time);
httpResp.setResults(results);
return httpResp;
}
}
package com.wnhz.auth.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 绑定信息和对应的代码
*/
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultCode{
BOOK_TYPE_SUCCESS(20010,"图书类型操作成功"),
PUBLISHER_SUCCESS(20100,"出版社操作成功"),
BOOK_SUCCESS(21000,"图书操作成功"),
USER_REGISTER_ERROR(10010,"注册失败"),
USER_REGISTER_SUCCESS(10001,"注册成功"),
GOODS_REDISSION_SUCCESS(00001,"REDISSION测试"),
USER_LOGIN_SUCCESS(30001,"用户登录成功"),
USER_NOT_LOGIN_ERROR(41000,"用户没有登录"),
USER_QUERY_SUCCESS(30003,"用户查询成功"),
USER_ADD_SUCCESS(10001,"用户添加成功"),
USER_ADD_ERROR(10002,"用户添加失败"),
USER_CODE_CREATE_SUCCESS(00002,"创建验证码成功"),
USER_CODE_SUCCESS(0003,"验证成功"),
USER_CODE_ERROR(0004,"验证失败"),
USER_CODE_NULL(0003,"验证码为空"),
USER_LOGIN_TOKEN_EXPIRED_ERROR(42000,"token异常"),
USER_PRIVILEGES_ERROR(45002,"权限异常")
;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
9.创建util层工具类
JwtUtil是对创建token和解析token的一个工具类
package com.wnhz.auth.util;
import cn.hutool.crypto.digest.DigestUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
private final static String key= DigestUtil.bcrypt("apple");
private static Jws<Claims> jws;
/**
* 创建token字符串
* @param username 用户名
* @param privs 权限名称
* @param expiredTime 过期时间
* @return
*/
public static String createToken(String username,String privs,long expiredTime){
String token = Jwts.builder().setHeaderParam("alg","SHA256")
.setHeaderParam("type","JWT")
.claim("username",username)
.claim("privs",privs)
.setExpiration(new Date(System.currentTimeMillis()+expiredTime))
.signWith(SignatureAlgorithm.HS256,key.getBytes())
.compact();
return token;
}
/**
* 解析令牌
* @param token
*/
public static void parasToken(String token){
jws = Jwts.parser().setSigningKey(key.getBytes())
.parseClaimsJws(token);
}
/**
* 取出claim中的值
* @param token
* @param key---->username/privs
* @return
*/
public static String getClaim(String token,String key){
parasToken(token);
return (String) jws.getBody().get(key);
}
}
PrivsCheck自己定义一个注解
package com.wnhz.auth.util;
import java.lang.annotation.*;
/**
* 自己定义一个注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrivsCheck {
String value() default "";
}
10.实现service层
service的接口
package com.wnhz.auth.service;
import com.wnhz.auth.entity.UserPrivs;
import java.util.List;
public interface IUserService {
List<UserPrivs> login(String username,String password);
List<String> findAllUsernames();
}
实现servece接口类
package com.wnhz.auth.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wnhz.auth.dao.IUserPrivsDao;
import com.wnhz.auth.entity.UserPrivs;
import com.wnhz.auth.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sun.dc.pr.PRError;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private IUserPrivsDao userPrivsDao;
@Override
public List<UserPrivs> login(String username, String password) {
QueryWrapper<UserPrivs> wrapper = new QueryWrapper<>();
wrapper.eq("user_username",username)
.eq("user_password",password);
return userPrivsDao.selectList(wrapper);
}
@Override
public List<String> findAllUsernames() {
List<String> usernames = new ArrayList<>();
userPrivsDao.selectList(null).forEach(
u->{usernames.add(u.getUsername());}
);
return usernames;
}
}
11.实现controller层
package com.wnhz.auth.controller;
import com.wnhz.auth.dto.HttpResp;
import com.wnhz.auth.dto.ResultCode;
import com.wnhz.auth.entity.UserPrivs;
import com.wnhz.auth.service.IUserService;
import com.wnhz.auth.util.JwtUtil;
import com.wnhz.auth.util.PrivsCheck;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Api(tags = "用户Api接口")
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private IUserService ius;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ApiOperation(value = "findAllUsernames",tags = "查询所有用户名接口")
@GetMapping("/findAllUsernames")
@PrivsCheck("findAllUsernames") //自己定义的注解,value值为权限名称,如何和数据库里的权限名称一致,说明有这个权限,数据库没有,则无权限
public HttpResp findAllUsernames(){
List<String> allUsernames = ius.findAllUsernames();
return HttpResp.result(ResultCode.USER_QUERY_SUCCESS,new Date(),allUsernames);
}
@ApiOperation(value = "login",tags = "用户登录接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "username",value = "用户名",required = true),
@ApiImplicitParam(name = "password",value = "密码",required = true)
})
@GetMapping("/login")
public HttpResp login(String username, String password, HttpServletResponse response){
List<UserPrivs> users = ius.login(username,password);
//字符穿拼接,获得登录用户的权限名称,用逗号隔开
StringBuilder sb = new StringBuilder();
users.forEach(u->sb.append(u.getPrivsname()+","));
sb.deleteCharAt(sb.length()-1);//去除最后一个多余的逗号
//创建token令牌
String token = JwtUtil.createToken(username,sb.toString(),1000*60);
stringRedisTemplate.opsForValue().set(username,token,3, TimeUnit.MINUTES);
response.addHeader("token",token);
return HttpResp.result(ResultCode.USER_LOGIN_SUCCESS,new Date(),username);
}
}
12.实现exception层(异常)
package com.wnhz.auth.exception;
/**
* 权限异常
*/
public class AccesRefusedException extends RuntimeException{
public AccesRefusedException(String message){
super(message);
}
}
package com.wnhz.auth.exception;
/**
* token的异常
*/
public class UserLoginExpiredException extends RuntimeException{
public UserLoginExpiredException(String message) {
super(message);
}
}
package com.wnhz.auth.exception;
/**
* 用户未登录异常
*/
public class UserNotLoginException extends RuntimeException {
public UserNotLoginException(String message){
super(message);
}
}
异常实现
package com.wnhz.auth.exception.handler;
import com.wnhz.auth.dto.HttpResp;
import com.wnhz.auth.dto.ResultCode;
import com.wnhz.auth.exception.AccesRefusedException;
import com.wnhz.auth.exception.UserLoginExpiredException;
import com.wnhz.auth.exception.UserNotLoginException;
import io.jsonwebtoken.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Date;
@Slf4j
@RestControllerAdvice
public class UserAutnExceptionHandler {
/**
* 用户没有登录异常
*/
@ExceptionHandler(UserNotLoginException.class)
public HttpResp<String> userNotLogin(UserNotLoginException e){
return HttpResp.result(ResultCode.USER_NOT_LOGIN_ERROR,new Date(),e.getMessage());
}
/**
* token过期
* @param e
* @return
*/
@ExceptionHandler(UserLoginExpiredException.class)
public HttpResp<String> userLoginExpired(UserLoginExpiredException e){
return HttpResp.result(ResultCode.USER_LOGIN_TOKEN_EXPIRED_ERROR,new Date(),e.getMessage());
}
/**
* token篡改
* @param e
* @return
*/
@ExceptionHandler(SignatureException.class)
public HttpResp<String> signatureException(SignatureException e){
return HttpResp.result(ResultCode.USER_LOGIN_TOKEN_EXPIRED_ERROR,new Date(),e.getMessage());
}
/**
* 权限异常
* @param e
* @return
*/
@ExceptionHandler(AccesRefusedException.class)
public HttpResp<String> accesRefusedException(AccesRefusedException e){
return HttpResp.result(ResultCode.USER_PRIVILEGES_ERROR,new Date(),e.getMessage());
}
}
13.实现interceptor层(拦截)
package com.wnhz.auth.interceptor;
import cn.hutool.jwt.JWTUtil;
import com.wnhz.auth.dto.ResultCode;
import com.wnhz.auth.exception.AccesRefusedException;
import com.wnhz.auth.exception.UserNotLoginException;
import com.wnhz.auth.util.JwtUtil;
import com.wnhz.auth.util.PrivsCheck;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 定义一个拦截器,获取token
* 1.token不存在,返回USER_NOT_LOGIN_ERROR,表示用户没有登录
* 2.token存在,正常情况利用handler和method获取所有方法,利用自定义的注解获得权限内容,与该用户的权限privs对比判断是否权限足够
* 如果有此权限则正常登录,没有则返回USER_PRIVILEGES_ERROR,表示权限异常
* 3.token存在,但过期了,如果redis里token任未过期,则重新创建一个token续期,利用JWTUtil.parseToken(token).getPayload().getClaim
* 任可以获得token的数据
* 如果redis里token也过期了,则抛出异常
*/
@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
response.setContentType("text/html;charset=utf-8");
if (Objects.isNull(token)) {
throw new UserNotLoginException(ResultCode.USER_NOT_LOGIN_ERROR.getMsg());
}
try {//正常情况
String username = JwtUtil.getClaim(token, "username");
String privs = JwtUtil.getClaim(token, "privs");
//判断是否能够范围当前的方法,java去反射
log.debug("用户权限:{}" , privs);
log.debug("handler: {}", handler.getClass());
HandlerMethod m = (HandlerMethod) handler;
Method method = m.getMethod();
log.debug("method: {}",method);//获得所有方法
PrivsCheck annotation = method.getAnnotation(PrivsCheck.class); //拿到方法上的注解,获取注解中的value值
System.out.println("------>" + annotation.value());
//contains:字符串匹配,如果privs中的字符串包含了annotation.value()的值,则返回true
if (privs.contains(annotation.value())) {//有此权限
return true;
} else {
throw new AccesRefusedException(ResultCode.USER_PRIVILEGES_ERROR.getMsg());
}
}
catch (ExpiredJwtException e) { //token过期
//续期
System.out.println(JWTUtil.parseToken(token).getPayload().getClaim("username"));
String username = (String) JWTUtil.parseToken(token).getPayload().getClaim("username");
String tprivs = (String) JWTUtil.parseToken(token).getPayload().getClaim("privs");
if (stringRedisTemplate.opsForValue().get(username)!=null){//redis的token没有过期
String nToken = JwtUtil.createToken(username, tprivs, 1000 * 30);
stringRedisTemplate.opsForValue().set(username, nToken, 3, TimeUnit.MINUTES);
response.setHeader("token", nToken);
return true;
}
else {
throw new UserNotLoginException("用户登录凭证失效");
}
}
}
}
14.实现config层(调用拦截)
package com.wnhz.auth.config;
import com.wnhz.auth.interceptor.AuthInterceptor;
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 AuthConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
/**
* 调用自己定义的拦截,拦截住api下的所有路径,除了user/login未拦截
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/user/login");
}
}
15.运行类
package com.wnhz.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthApp {
public static void main(String[] args) {
SpringApplication.run(AuthApp.class);
}
}
16.代码结构图
代码的一些解释请看代码中的注解