— 需要的技术 —
技术 | 说明 | 官网 |
---|---|---|
springboot | MVC框架 | https://spring.io/projects/spring-boot |
mybatis-plus | ORM框架 | https://baomidou.com |
SpringSecurity | 认证和授权框架 | https://spring.io/projects/spring-security |
Redis | 分布式缓存 | https://redis.io/ |
JWT | JWT登录支持 | https://github.com/jwtk/jjwt |
— 身份认证流程 —
— 依赖 —
<!-- spring-boot-starter-parent -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus-boot-starter 包含了mybatis的所有依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!--使用连接池的方式管理连接-->
<!--推荐大家使用阿里巴巴的druid的连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!--引入mysql数据库相关依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!-- spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.4</version>
</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>
<version>2.6.4</version>
</dependency>
<!--生成身份令牌-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--消除模板代码,生成getter、setter、构造器、toString()、equals()-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
— yml配置 —
server:
port: 8081
spring:
application:
name: manage-client
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
main:
allow-circular-references: true
redis:
database: 0
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 100
max-idle: 300
max-wait: 10000
cluster:
refresh:
adaptive: true
timeout: 5000
#mybatis-plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:com/huangyabei/mapper/xml/*.xml
— 工具类 —
JwtUtil
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Component
//@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {
/**
* 携带JWT令牌的HTTP的Header的名称,在实际生产中可读性越差越安全
*/
// @Getter
// @Value("${jwt.header}")
private String header = "Authorization";
/**
* 为JWT基础信息加密和解密的密钥
* 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
*/
// @Value("${jwt.secret}")
private String secret = "guYloAPAmKwvKq4a5f5dqnifiQatxMEPNOvtwPsJPQWLNKJDCXZ";
/**
* JWT令牌的有效时间,单位秒
* - 默认1周
*/
// @Value("${jwt.expiration}")
private Long expiration = 604800L;
/**
* SecretKey 根据 SECRET 的编码方式解码后得到:
* Base64 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
* Base64URL 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
* 未编码:SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
*/
private static SecretKey getSecretKey(String secret) {
byte[] encodeKey = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(encodeKey);
}
/**
* 用claims生成token
*
* @param claims 数据声明,用来创建payload的私有声明
* @return token 令牌
*/
private String generateToken(Map<String, Object> claims) {
SecretKey key = getSecretKey(secret);
//SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //两种方式等价
// 添加payload声明
JwtBuilder jwtBuilder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
// iat: jwt的签发时间
.setIssuedAt(new Date())
// 你也可以改用你喜欢的算法,支持的算法详见:https://github.com/jwtk/jjwt#features
// SignatureAlgorithm.HS256:指定签名的时候使用的签名算法,也就是header那部分
.signWith(SignatureAlgorithm.HS256, key)
.setExpiration(new Date(System.currentTimeMillis() + this.expiration * 1000));
String token = jwtBuilder.compact();
return token;
}
/**
* 生成Token令牌
*
* @param userDetails 用户
* @param id 用户编号
* @return 令牌Token
*/
public String generateToken(UserDetails userDetails, String id) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", id);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 从token中获取数据声明claim
*
* @param token 令牌token
* @return 数据声明claim
*/
public Claims getClaimsFromToken(String token) {
try {
SecretKey key = getSecretKey(secret);
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
return claims;
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
log.error("token解析错误", e);
throw new IllegalArgumentException("Token invalided.");
}
}
public String getUserId(String token) {
return (String) getClaimsFromToken(token).get("userId");
}
/**
* 从token中获取登录用户名
*
* @param token 令牌
* @return 用户名
*/
public String getSubjectFromToken(String token) {
String subject;
try {
Claims claims = getClaimsFromToken(token);
subject = claims.getSubject();
} catch (Exception e) {
subject = null;
}
return subject;
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 判断token是否过期
*
* @param token 令牌
* @return 是否过期:已过期返回true,未过期返回false
*/
public Boolean isTokenExpired(String token) {
Date expiration = getExpirationFromToken(token);
return expiration.before(new Date());
}
/**
* 验证令牌:判断token是否非法
*
* @param token 令牌
* @param userDetails 用户
* @return 如果token未过期且合法,返回true,否则返回false
*/
public Boolean validateToken(String token, UserDetails userDetails) {
//如果已经过期返回false
if (isTokenExpired(token)) {
return false;
}
String usernameFromToken = getSubjectFromToken(token);
String username = userDetails.getUsername();
return username.equals(usernameFromToken);
}
}
Redis
RedisConfig
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
RedisUtil
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
try {
return redisTemplate.keys(pattern);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(final String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(final String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(final String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public <T> T get(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return key == null ? null : operation.get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public <T> boolean set(final String key,final T value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入, 不存在放入,存在返回
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public <T> boolean setnx(final String key,final T value) {
try {
redisTemplate.opsForValue().setIfAbsent(key,value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public <T> boolean set(final String key,final T value,final long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间,不存在放入,存在返回
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public <T> boolean setnx(final String key,final T value,final long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(final String key,final long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(final String key,final long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public <T> T hget(final String key,final String item) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public <T> Map<String, T> hmget(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public <T> boolean hmset(final String key,final Map<String, T> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public <T> boolean hmset(final String key,final Map<String, T> map,final long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public <T> boolean hset(final String key,final String item,final T value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public <T> boolean hset(final String key,final String item,final T value,final long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(final String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(final String key,final String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(final String key,final String item,final double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(final String key,final String item,final double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public <T> Set<T> sGet(final String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public <T> boolean sHasKey(final String key,final T value) {
try {
if (null == value){
return false;
}
SetOperations setOperations = redisTemplate.opsForSet();
return setOperations.isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public <T> BoundSetOperations<String, T> sSet(final String key, final Set<T> dataSet) {
try {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public <T> BoundSetOperations<String, T> sSetAndTime(final String key,final long time,final Set<T> dataSet) {
try {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
if (time > 0)
expire(key, time);
return setOperation;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(final String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(final String key,Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
WebUtil
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class WebUtil {
/**
* 将字符串渲染到客户端
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static void renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
}
public static void setDownLoadHeader(String filename, ServletContext context, HttpServletResponse response) throws UnsupportedEncodingException {
String mimeType = context.getMimeType(filename);//获取文件的mime类型
response.setHeader("content-type",mimeType);
String fname= URLEncoder.encode(filename,"UTF-8");
response.setHeader("Content-disposition","attachment; filename="+fname);
}
}
-----------------------------------------核心代码部分--------------------------------------------
我们可以自定义一个UserDetailsService,让SpringSecrity使用我们的UserDetailsService,我们自己的UserDetailsService可以从数据库中查询用户名/密码/用户权限
— Entity —
普通数据库表实体类(可以根据自己业务需要设置user实体)
/**
* <p>
* 用户表
* </p>
*
* @author car-hailing-saas
* @since 2022-04-12
*/
@Data
@TableName("t_tenant_user")
public class TenantUserEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 姓名
*/
private String realName;
/**
* 密码
*/
private String passWord;
/**
* 租户ID
*/
private Long tenantId;
/**
* 性别 0男 1女
*/
private Integer sex;
/**
* 手机号
*/
private String mobile;
/**
* 状态:(0:禁用,1:正常)
*/
private Integer state;
/**
* 是否为超级管理员(0:否,1:是)
*/
private Integer ifSuper;
/**
* 头像
*/
private String headImage;
/**
* 邮箱
*/
private String email;
/**
* 联系电话
*/
private String telephone;
/**
* 公司所在城市编号
*/
private String cityNo;
/**
* 公司详细地址
*/
private String detailedAddress;
/**
* 组织机构ID
*/
private Long organizationId;
/**
* 职位ID
*/
private Long positionId;
/**
* 工号
*/
private String jobNumber;
/**
* 是否首次修改密码 0 首次 1 非首次
*/
private Integer firstUpdatePwd;
/**
*删除状态(0:正常,1:删除)
*/
private Integer delState;
/**
* 创建人ID
*/
private Long createBy;
/**
* 创建时间
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
private Date createTime;
/**
* 更新者ID
*/
private Long updateBy;
/**
* 更新的时间
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
private Date updateTime;
}
自定义实现 SpringSecrity的 UserDetails 接口的实体
import com.alibaba.fastjson.annotation.JSONField;
import com.car.hailing.saas.model.entity.TenantUserEntity;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginTenantUserEntity implements UserDetails {
/**
*用户对象(这里是自己的用户实体,对应数据库中user的实体,根据需要进行修改)
*/
private TenantUserEntity tenantUserEntity;
/**
* 权限集合(用来存从数据库查到的用户权限))
*/
private List<String> permissions;
//忽略序列化 SpringSecrity权限校验需要的格式的权限集合
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginTenantUserEntity(TenantUserEntity tenantUserEntity, List<String> permissions) {
this.tenantUserEntity = tenantUserEntity;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (!CollectionUtils.isEmpty(authorities)){
return authorities;
}
//把 permissions 中的String类型权限信息,封装成SimpleGrantedAuthority对象
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
//将用户实体中的用户密码指向 UserDetails的getPassword方法
return tenantUserEntity.getPassWord();
}
@Override
public String getUsername() {
//将用户实体中的用户名指向 UserDetails的getPassword方法
return tenantUserEntity.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
— UserDetailsServiceImpl —
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.car.hailing.saas.dao.TenantUserMapper;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.model.entity.TenantUserEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//根据自己业务需要定义查询接口
@Resource
TenantUserMapper tenantUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息 根据自己业务需要定义查询接口
QueryWrapper<TenantUserEntity> qw = new QueryWrapper<>();
qw.eq("user_name",username);
TenantUserEntity tenantUserEntity = tenantUserMapper.selectOne(qw);
if (Objects.isNull(tenantUserEntity)){
throw new UsernameNotFoundException("用户名未找到");
}
// TODO 查询对应的权限信息 根据自己业务需要定义查询接口
QueryWrapper<TenantUserEntity> qwMenys = new QueryWrapper<>();
qwMenys.eq("user_id",tenantUserEntity.getId());
List<String> list = tenantUserMapper.selectMenusList(qwMenys);
return new LoginTenantUserEntity(tenantUserEntity,list);
}
}
JwtAuthenticationTokenFilter 过滤器
import com.alibaba.fastjson.JSON;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.utils.JwtUtil;
import com.car.hailing.saas.utils.redis.RedisUtil;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* 认知过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
JwtUtil jwtUtil;
@Resource
RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)){
//这里的放行是请求中没有token而放行,比如登录接口这种白名单接口,会继续执行后面的过滤器
filterChain.doFilter(request,response);
return;
}
//2.解析token
Claims claims = jwtUtil.getClaimsFromToken(token);
Long userId = Long.valueOf(String.valueOf(claims.get("userId")));
//3.从redis中获取用户信息
Object userJson = redisUtil.hget("USER_TOKEN", userId + token);
//校验redis中是否存在user信息
if (Objects.isNull(userJson)){
throw new RuntimeException("用户未登录");
}
LoginTenantUserEntity loginTenantUserEntity = JSON.parseObject(String.valueOf(userJson), LoginTenantUserEntity.class);
//存入 SecurityContextHolder
// TODO 获取权限信息封装到 Authentication 中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginTenantUserEntity,null,loginTenantUserEntity.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//对请求放行
//这里的放行是已经对用户身份信息校验完毕了进行放行
filterChain.doFilter(request,response);
}
}
AuthenticationEntryPointHandler 授权认证异常处理器
import com.alibaba.fastjson.JSON;
import com.car.hailing.saas.base.CommonResult;
import com.car.hailing.saas.manage.utils.WebUtil;
import org.apache.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* security 授权认证异常处理器
*/
@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
/**
*身份认证失败处理
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//处理异常
WebUtil.renderString(response, JSON.toJSONString(new CommonResult<>(HttpStatus.SC_UNAUTHORIZED,"用户认证失败!")));
}
/**
*权限认证失败处理
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//处理异常
WebUtil.renderString(response, JSON.toJSONString(new CommonResult<>(HttpStatus.SC_FORBIDDEN,"用户权限不足!")));
}
}
**SecrityConfig 配置类 **
import com.car.hailing.saas.manage.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用Spring Security Global方法安全性类
public class SecrityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密/校验
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// anonymous()允许匿名访问 // permitAll() 任何人都可以访问 // denyAll() 任何人都不允许访问
// 基于注解配置接口权限 hasAuthority() 单个权限 hasAnyAuthority()多个权限
.antMatchers("/index/login").anonymous()
//除上面外的所有请求全部需要鉴权认证,指定任何身份验证的用户允许使用URL
.anyRequest().authenticated();
//将自定义的认证过滤器添加到secruty过滤器链最前面的位置
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
//添加异常处理器
http.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允许跨越
http.cors();
}
/**
* 身份验证管理器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
CorsConfig Spring boot 跨域配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring boot 跨域配置类
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨越的路径
registry.addMapping("/**")
//设置允许跨越请求的域名
.allowedOriginPatterns("*")
//是否允许cookie
.allowCredentials(true)
//设置允许的请求方式
.allowedMethods("GET","POST","DELETE","PUT")
//设置允许的header属性
.allowedHeaders("*")
//跨越允许时间 配置客户可以通过客户端缓存的飞行前请求的响应时间。
//默认情况下,这将设置为1800秒
.maxAge(900);
}
}
---------模拟登录登出接口实现----------
Controller
import com.car.hailing.saas.base.CommonResult;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.manage.service.LoginService;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/index")
public class LoginController {
@Autowired
public LoginService loginService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
private CommonResult login(@RequestBody LoginParam loginParam){
String token = loginService.login(loginParam);
return new CommonResult(ImmutableMap.of("token",token));
}
@RequestMapping(value = "/logout",method = RequestMethod.POST)
private CommonResult logout(HttpServletRequest request){
loginService.logout(request);
return new CommonResult();
}
}
Service
import com.baomidou.mybatisplus.extension.service.IService;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.model.entity.TenantUserEntity;
import javax.servlet.http.HttpServletRequest;
public interface LoginService extends IService<TenantUserEntity> {
/**
* 退出登录
*/
void logout(HttpServletRequest request);
/**
* 登录
* @param loginParam
* @return
*/
String login(LoginParam loginParam);
}
LoginServiceImpl
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.car.hailing.saas.dao.TenantUserMapper;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.manage.service.LoginService;
import com.car.hailing.saas.model.entity.TenantUserEntity;
import com.car.hailing.saas.utils.JwtUtil;
import com.car.hailing.saas.utils.redis.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
@Service
public class LoginServiceImpl extends ServiceImpl<TenantUserMapper, TenantUserEntity> implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Resource
JwtUtil jwtUtil;
@Resource
RedisUtil redisUtil;
@Override
public void logout(HttpServletRequest request) {
String token = request.getHeader("token");
String userId = jwtUtil.getUserId(token);
redisUtil.hdel("USER_TOKEN",userId+token);
}
@Override
public String login(LoginParam loginParam) {
//AuthenticationManager authenticate 进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginParam.getLoginName(),loginParam.getPassWord());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败!");
}
LoginTenantUserEntity loginTenantUserEntity = (LoginTenantUserEntity) authenticate.getPrincipal();
String userId = loginTenantUserEntity.getTenantUserEntity().getId().toString();
String token = jwtUtil.generateToken(loginTenantUserEntity, userId);
redisUtil.hset("USER_TOKEN", userId+token, loginTenantUserEntity, 604800L);
return token;
}
}
TenantUserMapper
/**
* <p>
* 租户用户表 Mapper 接口
* </p>
*
* @author car-hailing-saas
* @since 2022-04-12
*/
@Mapper
public interface TenantUserMapper extends BaseMapper<TenantUserEntity> {
}
认证失败的效果
— 自定义权限认证 (如果有特殊的权限验证要求可以自定义权限验证方法)—
***例如: ***
1.在实现了UserDetails接口的实体中定义一个需要验证的权限集合
LoginTenantUserEntity
/**
* 权限集合
*/
private List<String> permissions;
2.在实现了UserDetailsService的实现类中查询出需要校验的权限并添加到实体中(注意修改构造方法)
UserDetailsServiceImpl
// TODO 查询对应的权限信息
List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
return new LoginTenantUserEntity(tenantUserEntity,list);
3.编写自定义权限校验类
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 自定义权限校验
* 添加到spring容器中最好自定义一个Bean名称,这样在注解中用到就会比较清晰
*/
@Component("customize")
public class CustomizeExpressionRoot {
public boolean customizeHasAuthority(String authority){
//获取当前用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginTenantUserEntity loginTenantUserEntity = (LoginTenantUserEntity) authentication.getPrincipal();
List<String> permissions = loginTenantUserEntity.getPermissions();
//判断用户群权限集合中是否存在authority
return permissions.contains(authority);
}
}
Coenroller (注解模式验证添加权限验证)
@RestController
@RequestMapping("/test")
public class SecrityController {
@RequestMapping(value = "/index",method = RequestMethod.GET)
//Secrity 的权限验证方法不需要 加@ 符号
// @PreAuthorize("hasAuthority('test')")
//自定义的权限验证类 bean名字前需要加 @ 符号 test 是这个接口需要具备的访问权限
@PreAuthorize("@customize.customizeHasAuthority('test')")
public CommonResult test(){
return new CommonResult("OK");
}
}
注解的方法与配置用的方法是一样的,只是可以使用配置类方式或者是注解方式来配置
hasAuthority(String authority) //单个权限校验
hasAnyAuthority(String... authorities) //多个权限校验
hasRole(String role) //单个权限校验 这个方法会在传入的权限字符串前默认拼一个 defaultRolePrefix = "ROLE_";
hasAnyRole(String... roles) // 多个权限校验 这个方法会在传入的权限字符串前默认拼一个 defaultRolePrefix = "ROLE_";
isAnonymous() //允许匿名访问
permitAll() // 任何人都可以访问
denyAll() // 任何人都不允许访问
源码: