Springboot 整合JWT (token)+mybatis+自定义注解 实现简单的登录拦截模块

简介: Springboot 整合JWT (token)+mybatis+自定义注解 实现简单的登录拦截模块

这个实例的登录模块大概简单包含以下三个小功能:


用户注册


用户输入帐号密码,后台使用Spring Security的BCryptPasswordEncoder 进行密码加密,存库。


用户登录


用户输入帐号密码,后台查库使用Spring Security的BCryptPasswordEncoder进行密码校验,若登录成功,则返回JWT生成的token,带有过期时间。


token校验


用户访问其他接口,需要带着token访问,后台使用JWT token校验,错误或者过期则拦截,正常则继续访问。



那么接下来我们一起开始实现下。


项目最终目录结构:

image.png

首先准备个简单的数据库表 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;


凑合用下,大概这个样子:


image.png


接着pom.xml用到的核心jar:


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- druid数据源驱动 1.1.10解决springboot从1.0——2.0版本问题-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


然后是application.yml(里面的数据库连接url和driverClassName我使用了日志监控,你们不使用需要换一下):


如果想使用就导入依赖(使用这个东西的效果和教程可以参考我这篇,配合logback日志框架一起使用效果更佳  https://blog.csdn.net/qq_35387940/article/details/102563845):


        <!--监控sql日志-->
        <dependency>
            <groupId>org.bgee.log4jdbc-log4j2</groupId>
            <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
            <version>1.16</version>
        </dependency>
#配置项目名称
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<String, Object> 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先调用下注册接口,往数据库表里面添加下用户:


image.png


然后用这个帐号去调登录接口:


密码输错的时候,可以看到BCryptPasswordEncoder校验  出来了


image.png


那么输入正确的密码,可以看到登录成功后,token成功获取:


image.png


那么接下来带着token去访问其他接口,可以看到接口带的token是正常通过校验的:


image.png


在yml里面,咱们设置的过期时间值是20分钟,过期的时候,在拦截器里面也有相关的校验:


image.png


这些异常基本都已经被我提取出来了,检token过期的,检验token合理性的等等。


过期token返回示例:


image.png


错误token返回示例:


image.png


还有很多其他出错的情况,可以根据源码一个个提取,也可以直接不管,统一返回错误即可。


ok,这篇简单的整合jwt token登录注册模块教程就到此结束。

相关文章
|
19天前
|
JSON 安全 Java
什么是JWT?如何使用Spring Boot Security实现它?
什么是JWT?如何使用Spring Boot Security实现它?
70 5
|
2月前
|
JSON 安全 算法
|
1月前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
25天前
|
JSON 安全 算法
Spring Boot 应用如何实现 JWT 认证?
Spring Boot 应用如何实现 JWT 认证?
55 8
|
1月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
1月前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
141 1
|
1月前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
69 5
|
1月前
|
Java 数据库连接 mybatis
Mybatis使用注解方式实现批量更新、批量新增
Mybatis使用注解方式实现批量更新、批量新增
52 3
|
2月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
145 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
73 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
下一篇
DataWorks