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登录注册模块教程就到此结束。

相关文章
|
2月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
178 0
|
11天前
|
JSON NoSQL Java
springBoot:jwt&redis&文件操作&常见请求错误代码&参数注解 (九)
该文档涵盖JWT(JSON Web Token)的组成、依赖、工具类创建及拦截器配置,并介绍了Redis的依赖配置与文件操作相关功能,包括文件上传、下载、删除及批量删除的方法。同时,文档还列举了常见的HTTP请求错误代码及其含义,并详细解释了@RequestParam与@PathVariable等参数注解的区别与用法。
|
10天前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
16 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
11天前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
18天前
|
存储 JSON 算法
JWT令牌基础教程 全方位带你剖析JWT令牌,在Springboot中使用JWT技术体系,完成拦截器的实现 Interceptor (后附源码)
文章介绍了JWT令牌的基础教程,包括其应用场景、组成部分、生成和校验方法,并在Springboot中使用JWT技术体系完成拦截器的实现。
32 0
JWT令牌基础教程 全方位带你剖析JWT令牌,在Springboot中使用JWT技术体系,完成拦截器的实现 Interceptor (后附源码)
|
2月前
|
存储 JSON 前端开发
SpringBoot 如何实现无感刷新Token
【8月更文挑战第30天】在Web开发中,Token(尤其是JWT)作为一种常见的认证方式,被广泛应用于身份验证和信息加密。然而,Token的有效期问题常常导致用户需要重新登录,从而影响用户体验。为了实现更好的用户体验,SpringBoot可以通过无感刷新Token的机制来解决这一问题。以下将详细介绍SpringBoot如何做到无感刷新Token。
70 2
|
2月前
|
安全 Java 应用服务中间件
如何在 Spring Boot 3.3 中实现请求 IP 白名单拦截功能
【8月更文挑战第30天】在构建Web应用时,确保应用的安全性是至关重要的。其中,对访问者的IP地址进行限制是一种常见的安全措施,特别是通过实施IP白名单策略,可以只允许特定的IP地址或IP段访问应用,从而有效防止未授权的访问。在Spring Boot 3.3中,我们可以通过多种方式实现这一功能,下面将详细介绍几种实用的方法。
158 1
|
2月前
|
NoSQL 关系型数据库 MySQL
SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘
SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘
120 2
|
2月前
|
JSON JavaScript 前端开发
基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程
本文介绍了在SpringBoot + Vue项目中实现单个文件上传的同时携带Token和其它表单信息的前后端完整流程,包括后端SpringBoot的文件上传处理和前端Vue使用FormData进行表单数据和文件的上传。
192 0
基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程