基于token认证功能开发
引子:最近做项目时遇到了一个特殊的需求,需要写共享接口把本系统的一些业务数据共享给各地市的自建系统,为了体现公司的专业性以及考虑到程序的扩展性(通过各地市的行政区划代码做限制),决定要把接口做的高级一些,而不是简单的传个用户名和密码对比数据库里面的,那样真的很low。于是写了基于token的认证功能,在这里分享出来供大家学习与探讨。
1、项目初始化
项目的初始化很重要,我们需要事先准备好一些通用的工具类和配置类,便于后面开发。
因为新建工程比较简单,这里就不啰嗦了,看下我添加了那些GAV
坐标即可。
注意我用的SpringBoot
版本是3.0
的,如果版本和我保持一致的话pom.xml
也需要保持一致否则依赖可能下载不下来(SpringBoot3.0
当时还没有稳定版本的)。
1、pom.xml
<?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>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<!-- 所有SpringBoot项目都要继承spring-boot-starter-parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.laizhenghua</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<!-- 表示只会去仓库查找稳定版本(releases=true)不会去查找开发中的版本(snapshots=false) -->
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、统一返回类R
封装
为了避免API
返回的数据混乱,我们统一使用R
类进行返回,R
中返回的数据结构如下
{
"msg": "success", // 附加消息
"code": 200, // 状态码(可以自定义不一定完全与http状态码一样)
"data": "alex", // 数据统一放在data方便前端拦截器直接拦截data
"success": true // 成功标识(是否成功可以通过这个属性判断)
}
新建utils.R.java
(所有工具类都放在utils
包下)
R.java
/**
* TODO
*
* @Description 统一返回类封装
* @Author laizhenghua
* @Date 2023/2/19 20:04
**/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 563554414843661955L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R success(Object data, String msg) {
R r = new R();
r.put("code", 200);
r.put("data", data);
r.put("msg", msg);
r.put("success", true);
return r;
}
public static R success(Object data) {
return success(data, "success");
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
2、RedisTemplate序列化配置
RedisTemplate
默认采用JDK
的序列化方式,一是不支持跨语言,最重要的是出了问题,排查起来非常不方便!因此为了保证序列化不出问题,我们需要重新配置RedisTemplate
。
新建config.RedisConfiguration.java
(所有配置类都放在config
包下)
RedisConfiguration.java
/**
* TODO
*
* @Description RedisTemplate 序列化配置
* @Author laizhenghua
* @Date 2023/6/25 21:22
**/
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 我们为了开发方便直接使用<String,Object>泛型
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String序列化的配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化
template.setKeySerializer(stringRedisSerializer);
// Hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value采用Jackson2JsonRedisSerializer的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// Hash的value也采用jackson2JsonRedisSerializer的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
测试一下redis
缓存有没有问题
/**
* TODO
*
* @Description
* @Author laizhenghua
* @Date 2023/6/3 09:04
**/
@RestController
public class HelloController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/hello")
public R hello() {
redisTemplate.opsForValue().set("name", "alex");
return R.success(redisTemplate.opsForValue().get("name"));
}
}
浏览器访问这个API
,惊奇的发现自动跳转到了登录页面需要认证后才能访问API
。
认证方式也很简单可以输入用户名和密码进行认证,如
Username: user
Password: 启动项目控制台输出的uuid
// 如 Using generated security password: f4895be9-132b-4627-a7e6-25b9b5baeb1b
// 用户名为什么user?源码如下
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
...
public static class User {
/**
* Default user name.
*/
// 当然也可以通过配置文件去指定
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
...
}
}
3、SpringSecurity初步配置
以上除了在登录页面输入用户名和密码进行认证外,还有一种方式就是在请求头或其他地方增加token,通过解析token找到认证用户并给予认证(本文就是介绍这种方式)。
当然也可以配置这个请求不需要认证也不需要鉴权,这也是测试例子想引出的知识点,因为后面静态资源和一些特殊的请求是不需要认证的比如说swagger
相关的。
新建SecurityConfiguration.java
配置类
SecurityConfiguration.java
/**
* TODO
*
* @Description SecurityConfiguration
* @Author laizhenghua
* @Date 2023/6/25 21:55
**/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
// 1.需要注意的是SpringSecurity6.0版本不再是是继承WebSecurityConfigurerAdapter来配置HttpSecurity,而是使用SecurityFilterChain来注入
// 2.SpringSecurity6.0需要添加@EnableWebSecurity来开启一些必要的组件
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 关闭csrf因为不使用session
http.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
// 配置不需要认证的请求
.requestMatchers("/hello").permitAll()
// 除了上面那些请求都需要认证
.anyRequest().authenticated();
return http.build();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
}