token和session都可以用来做身份验证,而session用多了,对服务器的压力是很大的。
这里主要是生成一个uuid作为账户的身份令牌,用来校验用户是否登录。
项目里有用MyBatis-Plus,先简单说下MyBatis-Plus的配置,如果是用Mybatis之类的可以直接跳过这些配置
maven依赖相应的包
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.demo</groupId>
<artifactId>springboottest</artifactId>
<version>4.0.0</version>
<name>SpringBootTest</name>
<description>Demo project for Spring Boot</description>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<logback.version>1.1.3</logback.version>
<druid.version>1.1.13</druid.version>
<mybatisplus.version>3.0.7.1</mybatisplus.version>
<mysql.version>8.0.11</mysql.version>
<mssql.version>4.0</mssql.version>
<commons.lang.version>2.6</commons.lang.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- mssql驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>${mssql.version}</version>
</dependency>
<!-- postgresql驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 日志管理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
<version>0.1.4</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件:
server:
port: 8085
tomcat:
uri-encoding: UTF-8
max-threads: 1000
min-spare-threads: 30
servlet:
context-path: /demo
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=false
username: root
password: 123456
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
logging:
level.com.demo: debug
path: logs/
file: today.log
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
typeAliasesPackage: com.demo.springboottest.po
global-config:
#数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: NOT_NULL
#驼峰下划线转换
logic-delete-value: -1
logic-not-delete-value: 0
banner: false
#原生配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
mybatis-plus配置
package com.demo.springboottest.config;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* mybatis-plus配置
*
* @author lk
*/
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
}
用户 实体类
package com.demo.springboottest.po;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* app端用户表
*
* @author lk
* @date 2019-05-10 15:40:03
*/
@Data
@TableName("app_user")
public class UserEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@TableId
private Long userId;
/**
* app端用户昵称
*/
private String userName;
/**
* 电话号码
*/
private String mobile;
/**
* 登录密码
*/
private String password;
/**
* app用户编号
*/
private String userCode;
/**
* 用户性别0:男 1:女
*/
private Integer userSex;
/**
* 用户邮箱
*/
private String userEmail;
/**
* 用户头像url
*/
private String userHeadImage;
/**
* 省
*/
private String userProvince;
/**
* 市
*/
private String userCity;
/**
* 区
*/
private String userArea;
/**
* 身份证号
*/
private String userCertId;
/**
* 证件类型(默认1身份证)
*/
private Integer userCertType;
/**
* 详细地址
*/
private String userAddress;
/**
* 生日
*/
private Date userBirthday;
/**
* 用户状态1:正常 0失效
*/
private Integer userState;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
}
用户Token
package com.demo.springboottest.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 用户Token
*
* @author lk
* @email
* @date 2019-03-26 10:29:14
*/
@Data
@TableName("app_token")
public class TokenEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
*
*/
@TableId(type = IdType.INPUT)
private Long userId;
/**
* token
*/
private String token;
/**
* 过期时间
*/
private Date expireTime;
/**
* 更新时间
*/
private Date updateTime;
}
@Data 是lombok插件的注解,自动生成get、set、toString等
注意如果用了MyBatis-Plus创建的实体类必须加@TableName注解,否则会报错,当然你也得去建相应的表
配置一个自定以注解:@Login, 只要加了这个注解的就会校验用户的Token。
package com.demo.springboottest.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {
}
@LoginUser 用来获取用户信息,需要的话可以配置,也可以忽视的。
package com.demo.springboottest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 登录用户信息
*
* @author lk
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
用户登录的时候给当前用户生成或修改token
controller
package com.demo.springboottest.controller;
import com.demo.springboottest.annotation.Login;
import com.demo.springboottest.annotation.LoginUser;
import com.demo.springboottest.ao.LoginAO;
import com.demo.springboottest.exception.RRException;
import com.demo.springboottest.po.UserEntity;
import com.demo.springboottest.service.TokenService;
import com.demo.springboottest.service.UserService;
import com.demo.springboottest.util.ReturnResult;
import com.demo.springboottest.util.ValidatorUtils;
import io.swagger.annotations.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
import javax.annotation.Resource;
import java.util.Map;
/**
* @author lk
*/
@Api(tags = "登录接口")
@RestController
@RequestMapping("/user")
public class LoginController {
private Logger logger = LoggerFactory.getLogger(LoginController.class);
@Resource
private UserService userService;
@Resource
private TokenService tokenService;
@PostMapping("login")
@ApiOperation(value = "登录", notes = "用户登录成功,返回token和token失效时间")
@ApiImplicitParam(name = "loginAO", value = "用户登录参数", required = true, paramType = "body", dataType = "LoginAO")
public ReturnResult login(@RequestBody LoginAO loginAO) {
Map<String, Object> result;
logger.info("method : login(),params: " + loginAO.toString());
try {
//校验参数
ValidatorUtils.validateEntity(loginAO);
result = userService.login(loginAO);
} catch (RRException rre) {
//返回自定义异常
logger.error(rre.getMsg());
return ReturnResult.error(rre.getMsg());
}
return ReturnResult.ok(result);
}
@Login
@PostMapping("logout")
@ApiOperation(value = "登出", notes = "将该用户的token失效期设置为当前时间")
public ReturnResult logout(@ApiIgnore @RequestAttribute("userId") long userId) {
tokenService.expireToken(userId);
return ReturnResult.ok();
}
@Login
@GetMapping("userInfo")
@ApiOperation(value = "查询", notes = "获取用户信息")
public ReturnResult getUser(@ApiIgnore @LoginUser UserEntity userEntity) {
return ReturnResult.ok().put("user", userEntity);
}
}
service
package com.demo.springboottest.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.demo.springboottest.ao.LoginAO;
import com.demo.springboottest.dao.UserMapper;
import com.demo.springboottest.exception.RRException;
import com.demo.springboottest.po.TokenEntity;
import com.demo.springboottest.po.UserEntity;
import com.demo.springboottest.service.TokenService;
import com.demo.springboottest.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {
@Resource
private TokenService tokenService;
@Override
public Map<String, Object> login(LoginAO loginAO) {
Map<String, Object> result = new HashMap<>(2);
String mobile = loginAO.getMobile();
UserEntity user = queryUser(mobile);
if (null != user) {
if (!user.getPassword().equals(loginAO.getPassword())) {
throw new RRException("密码错误!");
}
}else {
throw new RRException("该账户不存在!");
}
//获取登录token
TokenEntity tokenEntity = tokenService.createToken(user.getUserId());
result.put("token", tokenEntity.getToken());
result.put("expire", tokenEntity.getExpireTime().getTime() - System.currentTimeMillis());
return result;
}
private UserEntity queryUser(String mobile) {
return baseMapper.selectOne(new QueryWrapper<UserEntity>().eq("mobile", mobile));
}
}
生成token createToken(long id),登出的时候调expireToken(long userId) 使当前用户的token失效时间设置为当前时间
package com.demo.springboottest.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.demo.springboottest.dao.TokenMapper;
import com.demo.springboottest.po.TokenEntity;
import com.demo.springboottest.service.TokenService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.UUID;
@Service
public class TokenServiceImpl extends ServiceImpl<TokenMapper, TokenEntity> implements TokenService {
/**
* 12小时后过期
*/
private final static int EXPIRE = 3600 * 12;
@Override
public TokenEntity createToken(long id) {
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
//生成token
String token = UUID.randomUUID().toString().replace("-", "");
//保存或更新用户token
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setUserId(id);
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
saveOrUpdate(tokenEntity);
return tokenEntity;
}
@Override
public void expireToken(long userId) {
Date now = new Date();
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setUserId(userId);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(now);
saveOrUpdate(tokenEntity);
}
@Override
public TokenEntity queryByToken(String token) {
return getOne(new QueryWrapper<TokenEntity>().eq("token", token));
}
}
当然还需要配置一个拦截器,拦截只要加了@Login注解的方法就会校验用户的token
拦截器:
package com.demo.springboottest.interceptor;
import com.demo.springboottest.annotation.Login;
import com.demo.springboottest.exception.RRException;
import com.demo.springboottest.po.TokenEntity;
import com.demo.springboottest.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author LK
*/
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private TokenService tokenService;
public static final String USER_KEY = "userId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Login annotation;
if(handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
}else{
return true;
}
if(annotation == null){
return true;
}
//从header中获取token
String token = request.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = request.getParameter("token");
}
//token为空
if(StringUtils.isBlank(token)){
throw new RRException(403,"token不能为空");
}
//查询token信息
TokenEntity tokenEntity = tokenService.queryByToken(token);
if (null != tokenEntity && StringUtils.isNotBlank(tokenEntity.toString())) {
//token失效
if (tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
throw new RRException(401,"请重新登录");
}
} else {
throw new RRException(402,"该用户或已重新登录");
}
//设置userId到request里,后续根据userId,获取用户信息
request.setAttribute(USER_KEY, tokenEntity.getUserId());
return true;
}
}
@LoginUser 获取用户信息
package com.demo.springboottest.resolver;
import com.demo.springboottest.annotation.LoginUser;
import com.demo.springboottest.interceptor.AuthorizationInterceptor;
import com.demo.springboottest.po.UserEntity;
import com.demo.springboottest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* 有@LoginUser注解的方法参数,注入当前登录用户
*
* @author lk
*/
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
//获取用户ID
Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
if (object == null) {
return null;
}
//获取用户信息
return userService.getById((Long) object);
}
}
WebMvcConfigurer
package com.demo.springboottest.config;
import com.demo.springboottest.interceptor.AuthorizationInterceptor;
import com.demo.springboottest.resolver.LoginUserHandlerMethodArgumentResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* MVC配置
*
* @author lk
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Autowired
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
}
}
需要注意的是/** 不可以设置为/demo/** ,/demo 为context-path 否则会使拦截器失效
异常处理器
package com.demo.springboottest.util;
import com.demo.springboottest.exception.RRException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 异常处理器
*
* @author lk
*/
@RestControllerAdvice
public class RRExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理自定义异常
*/
@ExceptionHandler(RRException.class)
public ReturnResult handleRRException(RRException e){
ReturnResult r = new ReturnResult();
r.put("code", e.getCode());
r.put("msg", e.getMessage());
return r;
}
@ExceptionHandler(DuplicateKeyException.class)
public ReturnResult handleDuplicateKeyException(DuplicateKeyException e){
logger.error(e.getMessage(), e);
return ReturnResult.error("数据库中已存在该记录");
}
@ExceptionHandler(Exception.class)
public ReturnResult handleException(Exception e){
logger.error(e.getMessage(), e);
return ReturnResult.error();
}
}
在swagger配置里添加securitySchemes 可以在swagger页面设置header
package com.demo.springboottest.config;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
/**
* Swagger配置
*
* @author lk
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//加了ApiOperation注解的类,生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
//包下的类,生成接口文档
.paths(PathSelectors.any())
.build()
.securitySchemes(security());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swagger")
.description("spring boot swagger test")
.termsOfServiceUrl("localhost:8085/demo")
.version("4.0.0")
.build();
}
private List<ApiKey> security() {
return newArrayList(
new ApiKey("token", "token", "header")
);
}
}
访问接口没有设置token的时候
获取token
将其复制粘贴到
设置token后访问接口