这个实例的登录模块大概简单包含以下三个小功能:
用户注册
用户输入帐号密码,后台使用Spring Security的BCryptPasswordEncoder 进行密码加密,存库。
用户登录
用户输入帐号密码,后台查库使用Spring Security的BCryptPasswordEncoder进行密码校验,若登录成功,则返回JWT生成的token,带有过期时间。
token校验
用户访问其他接口,需要带着token访问,后台使用JWT token校验,错误或者过期则拦截,正常则继续访问。
那么接下来我们一起开始实现下。
项目最终目录结构:
首先准备个简单的数据库表 user_info:
CREATE TABLE `user_info` (
`UI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键自增',
`UI_USER_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`UI_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户密码',
`UI_STATUS` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'O' COMMENT 'O:正常,D:已删除',
`UI_CREATE_TIME` bigint(12) NULL DEFAULT NULL COMMENT '用户创建时间',
`UI_ROLES` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`UI_ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
凑合用下,大概这个样子:
接着pom.xml用到的核心jar:
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
com.alibaba
druid-spring-boot-starter
1.1.10
org.springframework.boot
spring-boot-starter-security
com.auth0
java-jwt
3.4.0
org.projectlombok
lombok
1.16.10
org.springframework.boot
spring-boot-starter-test
test
然后是application.yml(里面的数据库连接url和driverClassName我使用了日志监控,你们不使用需要换一下):
如果想使用就导入依赖(使用这个东西的效果和教程可以参考我这篇,配合logback日志框架一起使用效果更佳 https://blog.csdn.net/qq_35387940/article/details/102563845):
org.bgee.log4jdbc-log4j2
log4jdbc-log4j2-jdbc4.1
1.16
#配置项目名称
spring:
application:
name: ElegantDemo
#数据库连接
datasource:
druid:
username: root
password: root
url: jdbc:log4jdbc:mysql://localhost:3306/mylocal?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull
driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#配置端口
server:
port: 8037
#单位 分钟
EXPIRE_TIME: 20
接着,我们先创建2个注解,分别是CheckToken 和 PassToken,用于更加灵活地标注哪些接口需要校验token,哪些不需要校验:
CheckToken.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
boolean required() default true;
}
/*
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包*/
/*
@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解*/
PassToken.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
/*
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包*/
/*
@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解*/
然后是弄个登录拦截器,这里主要用于校验token,AuthenticationInterceptor.java:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.demo.elegant.jwtToken.PassToken;
import com.demo.elegant.jwtToken.CheckToken;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserInfoService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passToken注解,有则无需进行token校验
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有CheckToken的注解
if (method.isAnnotationPresent(CheckToken.class)) {
CheckToken CheckToken = method.getAnnotation(CheckToken.class);
if (CheckToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("您的token已坏掉了,请重新登录获取token");
}
User user = userService.getUserInfoById(Integer.valueOf(userId));
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getUI_PASSWORD())).build();
try {
jwtVerifier.verify(token);
}catch (InvalidClaimException e){
throw new RuntimeException("无效token,请重新登录获取token");
}catch (TokenExpiredException e){
throw new RuntimeException("token已过期,请重新登录获取token");
} catch (JWTVerificationException e) {
throw new RuntimeException(e.getMessage());
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
拦截器手动配置类,InterceptorConfig.java:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
}
然后是普遍登录流程里需要用到的(包含token生成和解析方法,密码加密解密方法等等),
pojo :
User.java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author : JCccc
* @CreateTime : 2019/11/26
* @Description :
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer UI_ID;
private String UI_USER_NAME;
private String UI_PASSWORD;
private String UI_STATUS;
private Long UI_CREATE_TIME;
private String UI_ROLES;
}
mapper:
UserMapper.java
这里为了方便,我就采取mybatis注解方式去操作数据库了。
import com.demo.elegant.pojo.User;
import org.apache.ibatis.annotations.*;
/**
* @Author : JCccc
* @CreateTime : 2019/11/26
* @Description :
**/
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user_info WHERE UI_ID=#{userId}")
User getUserInfoById(@Param("userId") Integer userId);
@Select("SELECT * FROM user_info WHERE UI_USER_NAME=#{userName}")
User getUserInfoByName(@Param("userName") String userName);
@Insert("INSERT INTO user_info ( UI_USER_NAME, UI_PASSWORD, UI_STATUS,UI_CREATE_TIME, UI_ROLES ) VALUES ( #{UI_USER_NAME}, #{UI_PASSWORD},#{UI_STATUS},#{UI_CREATE_TIME},#{UI_ROLES}) ")
@Options(useGeneratedKeys = true, keyProperty = "UI_ID")
int addUser( User User);
}
service:
UserInfoService.java
import com.demo.elegant.pojo.User;
/**
* @Author : JCccc
* @CreateTime : 2019/11/26
* @Description :
**/
public interface UserInfoService {
User getUserInfoById( Integer userId);
User getUserInfoByName( String userName);
int addUser( User User);
}
TokenService
import com.demo.elegant.pojo.User;
import java.util.Date;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
public interface TokenService {
public String getToken(User user, Date date);
}
serviceImpl:
UserInfoServiceImpl.java
import com.demo.elegant.mapper.UserMapper;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Author : JCccc
* @CreateTime : 2019/11/26
* @Description :
**/
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
UserMapper userMapper;
@Override
public User getUserInfoById(Integer userId) {
return userMapper.getUserInfoById(userId);
}
@Override
public User getUserInfoByName(String userName) {
return userMapper.getUserInfoByName(userName);
}
@Override
public int addUser(User User) {
return userMapper.addUser(User);
}
}
TokenServiceImpl.java
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.TokenService;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @Author : JCccc
* @CreateTime : 2019/11/27
* @Description :
**/
@Service
public class TokenServiceImpl implements TokenService {
@Override
public String getToken(User user, Date date) {
String token="";
token= JWT.create()
.withAudience(String.valueOf(user.getUI_ID()))
.withExpiresAt(date) //过期时间配置
.sign(Algorithm.HMAC256(user.getUI_PASSWORD()));
return token;
}
}
最后是我们的登录接口,注册接口, UserInfoController.java:
import com.demo.elegant.jwtToken.PassToken;
import com.demo.elegant.jwtToken.CheckToken;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.TokenService;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Author : JCccc
* @CreateTime : 2019/11/26
* @Description :
**/
@RestController
@RequestMapping("/user")
public class UserInfoController {
@Autowired
UserInfoService userService;
@Autowired
TokenService tokenService;
@Value("${EXPIRE_TIME}")
private String EXPIRE_TIME;
@CheckToken
@GetMapping("/getUserByName/{userName}")
public String getUser(@PathVariable("userName") String userName) {
User userInfoByName = userService.getUserInfoByName(userName);
return userInfoByName.toString();
}
//注册
@PassToken
@PostMapping("/register")
public String register(@RequestBody Map map) {
BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
String encodePwd = bCryptPasswordEncoder.encode(String.valueOf(map.get("password")));
User User=new User();
User.setUI_USER_NAME(String.valueOf(map.get("username")));
User.setUI_PASSWORD(encodePwd);
User.setUI_STATUS("0");
User.setUI_CREATE_TIME(System.currentTimeMillis());
User.setUI_ROLES(String.valueOf(map.get("roles")));
int i = userService.addUser(User);
if (i==1){
return "注册成功";
}
return "注册失败";
}
//登录
@PostMapping("/login")
public Map login(@RequestBody Map user){
Map result=new HashMap();
User userForBase=userService.getUserInfoByName(String.valueOf(user.get("username")));
if(userForBase==null){
result.put("message","登录失败,用户不存在");
return result;
}else {
BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
String dbPwd=userForBase.getUI_PASSWORD();
boolean matchesResult = bCryptPasswordEncoder.matches(String.valueOf(user.get("password")),dbPwd);
if (!matchesResult){
result.put("message","登录失败,密码错误");
return result;
}else {
Date expiresDate = new Date(System.currentTimeMillis()+Integer.valueOf(EXPIRE_TIME)*60*1000);
String token = tokenService.getToken(userForBase,expiresDate);
result.put("token", token);
result.put("expireTime", EXPIRE_TIME);
result.put("userId", userForBase.getUI_ID());
return result;
}
}
}
@CheckToken
@GetMapping("/afterLogin")
public String afterLogin(){
return "你已通过验证,成功进入系统";
}
}
PS: 注册接口里面所使用的密码加密与登录接口校验时使用的密码校验都是用的Spring Security的BCryptPasswordEncoder。
最后在启动类上,让Spring Security不要自动装载 ,毕竟这里只想用下BCryptPasswordEncoder。
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class ElegantApplication {
public static void main(String[] args) {
SpringApplication.run(ElegantApplication.class, args);
}
}
最后就是简单的测试了,用postman先调用下注册接口,往数据库表里面添加下用户:
然后用这个帐号去调登录接口:
密码输错的时候,可以看到BCryptPasswordEncoder校验 出来了
那么输入正确的密码,可以看到登录成功后,token成功获取:
那么接下来带着token去访问其他接口,可以看到接口带的token是正常通过校验的:
在yml里面,咱们设置的过期时间值是20分钟,过期的时候,在拦截器里面也有相关的校验:
这些异常基本都已经被我提取出来了,检token过期的,检验token合理性的等等。
过期token返回示例:
错误token返回示例:
还有很多其他出错的情况,可以根据源码一个个提取,也可以直接不管,统一返回错误即可。
ok,这篇简单的整合jwt token登录注册模块教程就到此结束。