JWT动态权限
1、 表结构
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(100) DEFAULT NULL COMMENT '账号',
`password` varchar(100) DEFAULT NULL COMMENT '登录密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理-用户基础信息表';
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$mL9RkiiwitU38OhhV2KNOejmCqNABy5BYCsXEx80Ry5qWvIj3UH5G', '2019-05-05 16:09:06', '2019-09-19 00:59:47');
INSERT INTO `sys_user` VALUES (2, 'acton', '$2a$10$mL9RkiiwitU38OhhV2KNOejmCqNABy5BYCsXEx80Ry5qWvIj3UH5G', '2019-05-05 16:15:06', '2019-09-19 01:47:19');
INSERT INTO `sys_user` VALUES (3, 'test', '$2a$10$mL9RkiiwitU38OhhV2KNOejmCqNABy5BYCsXEx80Ry5qWvIj3UH5G', '2023-11-02 19:54:21', '2023-11-02 19:54:26');
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` varchar(50) DEFAULT NULL COMMENT '角色编码',
`name` varchar(50) DEFAULT NULL COMMENT '角色名称',
`remarks` varchar(100) DEFAULT NULL COMMENT '角色描述',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理-角色表 ';
INSERT INTO `sys_role` VALUES (1, 'admin', '系统管理员', '系统管理员', '2019-03-28 15:51:56', '2019-03-28 15:51:59');
INSERT INTO `sys_role` VALUES (2, 'visitor', '访客', '访客', '2019-03-28 20:17:04', '2019-09-09 16:32:15');
INSERT INTO `sys_role` VALUES (3, 'test', '测试', '测试', '2023-11-02 19:55:07', '2023-11-15 19:55:13');
CREATE TABLE `sys_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理 - 用户角色关联表 ';
INSERT INTO `sys_user_role` VALUES (1, 1, 1, '2019-08-21 10:49:41', '2019-08-21 10:49:41');
INSERT INTO `sys_user_role` VALUES (2, 2, 2, '2019-09-18 21:26:32', '2019-09-18 21:26:32');
INSERT INTO `sys_user_role` VALUES (3, 3, 2, '2019-09-18 21:26:32', '2019-09-18 21:26:32');
INSERT INTO `sys_user_role` VALUES (4, 3, 3, '2023-11-02 20:01:35', '2023-11-02 20:01:39');
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`url` varchar(50) DEFAULT NULL COMMENT 'url',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理-权限资源表 ';
INSERT INTO `sys_permission` VALUES (1, '/admin/**', '2019-03-28 18:51:08', '2019-03-28 18:51:10');
INSERT INTO `sys_permission` VALUES (2, '/test/**', '2019-03-28 18:52:13', '2019-08-31 21:26:57');
INSERT INTO `sys_permission` VALUES (3, '/a/**', '2019-03-28 18:52:13', '2019-03-28 18:52:13');
INSERT INTO `sys_permission` VALUES (4, '/visitor/**', '2023-11-02 20:02:28', '2023-11-02 20:02:32');
CREATE TABLE `sys_role_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`permission_id` int(11) DEFAULT NULL COMMENT '权限ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理 - 角色-权限资源关联表 ';
INSERT INTO `sys_role_permission` VALUES (1, 1, 1, '2019-09-18 21:06:26', '2019-09-18 21:06:26');
INSERT INTO `sys_role_permission` VALUES (2, 1, 2, '2019-09-18 21:06:27', '2019-09-18 21:06:27');
INSERT INTO `sys_role_permission` VALUES (3, 1, 3, '2019-09-18 21:06:27', '2019-09-18 21:06:27');
INSERT INTO `sys_role_permission` VALUES (4, 1, 4, '2019-09-18 21:26:43', '2019-09-18 21:26:43');
INSERT INTO `sys_role_permission` VALUES (5, 2, 3, '2019-09-18 21:26:43', '2019-09-18 21:26:43');
INSERT INTO `sys_role_permission` VALUES (6, 2, 4, '2023-11-02 20:04:09', '2023-11-02 20:04:13');
INSERT INTO `sys_role_permission` VALUES (7, 3, 2, NULL, NULL);
2、依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
3、项目结构
4、application.yml
server:
port: 8080
spring:
#数据源
datasource:
url: jdbc:mysql://localhost:3306/webmagic?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#redis
redis:
database: 0
host: localhost
port: 6379
password: 123456
lettuce:
pool:
max-active: 8 #最大连接数
max-idle: 8 #最大空闲连接数
max-wait: -1ms #最大阻塞等待事件,默认为-1,表示没有限制
min-idle: 0 #最小空闲连接数
#mybatis配置
mybatis:
typeAliasesPackage: pers.zhang.pojo
mapperLocations: classpath:mapper/*.xml
#configLocation: classpath:/mybatis-config.xml
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(默认30分钟)
expireTime: 30
4、实体
LoginUser:登录对象,继承UserDetails用于Security认证。该对象会被缓存到redis中,不需要序列化的字段加上@JsonIgnore注解。
public class LoginUser implements UserDetails {
private SysUser user;
private Long loginTime;
private Long expireTime;
private List<SysRole> roles;
public LoginUser() {
}
public LoginUser(SysUser user, List<SysRole> roles) {
this.user = user;
this.roles = roles;
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (!CollectionUtils.isEmpty(this.roles)) {
for (SysRole role : this.roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode());
authorities.add(authority);
}
}
return authorities;
}
@JsonIgnore
@Override
public String getPassword() {
return user.getPassword();
}
@JsonIgnore
@Override
public String getUsername() {
return user.getUsername();
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
public SysUser getUser() {
return user;
}
public void setUser(SysUser user) {
this.user = user;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
}
SysUser:系统用户
@Data
public class SysUser{
private Long id;
private String username;
private String password;
private Date createTime;
private Date updateTime;
}
SysRole:角色对象
@Data
public class SysRole {
private Long id;
private String code;
private String name;
private String remarks;
private Date createTime;
private Date updateTime;
}
SysPermission:权限对象
@Data
public class SysPermission {
private Long id;
private String url;
private Date createTime;
private Date updateTime;
}
5、常量
public class CacheConstants {
/**
* 登录用户 redis key
*/
public static final String LOGIN_TOKEN_KEY = "login_tokens:";
}
public class Constants {
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
}
public class HttpStatus {
/**
* 操作成功
*/
public static final int SUCCESS = 200;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
public static final int WARN = 601;
}
6、配置
6.1、Redis
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用GenericJackson2JsonRedisSerializer 替换默认序列化(默认采用的是JDK序列化)
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
6.2、WebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}
6.3、Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 获取访问url所需要的角色信息,自定义anti风格匹配
*/
@Autowired
private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
/**
* 认证权限处理 - 将上面所获得角色权限与当前登录用户的角色做对比,如果包含其中一个角色即可正常访问
*/
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
/**
* 自定义访问无权限接口时403响应内容
*/
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类 和 鉴权失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(customAccessDeniedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
//自定义url权限认证处理
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(customAccessDecisionManager);
return o;
}
})
// 对于登录login 注册register 允许匿名访问
.antMatchers("/login", "/register").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过Spring Security过滤器链
* http.permitAll(): 不会绕开springsecurity验证,相当于是允许该路径通过
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET, "/favicon.ico", "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**")
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**");
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
7、缓存
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @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);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
8、Service
8.1、TokenService
负责生成、刷新JWT Token等操作。
@Component
public class TokenService
{
private static final Logger log = LoggerFactory.getLogger(TokenService.class);
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Autowired
private RedisCache redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StrUtil.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
Integer userId = (Integer) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(new Long(userId));
LoginUser user = redisCache.getCacheObject(userKey);
return user;
} catch (Exception e) {
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser) {
if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNotEmpty(loginUser.getUser())) {
refreshToken(loginUser);
}
}
/**
* 删除用户身份信息
*/
public void delLoginUser(Long userId) {
if (ObjectUtil.isNotEmpty(userId)) {
String userKey = getTokenKey(userId);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, loginUser.getUser().getId());
return createToken(claims);
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据userid将loginUser缓存
String userKey = getTokenKey(loginUser.getUser().getId());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(Long id) {
return CacheConstants.LOGIN_TOKEN_KEY + id;
}
}
8.2、SysUserService
实现了UserDetailsService,loadUserByUsername方法会在认证过程中被回调。
@Service
public class SysUserService implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleService sysRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<SysUser> userList = sysUserMapper.loadUserByUsername(username);
SysUser user = null;
// 判断用户是否存在
if (!CollectionUtils.isEmpty(userList)) {
user = userList.get(0);
} else {
throw new UsernameNotFoundException("用户名不存在!");
}
//返回一个可被缓存的LoginUser
return new LoginUser(user, sysRoleService.listRolesByUserId(user.getId()));
}
}
8.3、SysRoleService
@Service
public class SysRoleService {
@Autowired
private SysRoleMapper sysRoleMapper;
public List<SysRole> listRolesByUserId(Long userId) {
return sysRoleMapper.listRolesByUserId(userId);
}
}
8.4、SysPermissionService
@Service
public class SysPermissionService {
@Autowired
private SysPermissionMapper sysPermissionMapper;
public List<SysPermission> listAll() {
return sysPermissionMapper.listAll();
}
public List<SysRole> getRolesByPermissionId(Long permissionId) {
return sysPermissionMapper.getRolesByPermissionId(permissionId);
}
}
8.5、LoginService
@Service
public class LoginService {
@Autowired
private TokenService tokenService;
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
public String login(String username, String password) {
// 用户名或密码为空
if (StrUtil.isEmpty(username) || StrUtil.isEmpty(password)) {
throw new RuntimeException("用户名或密码为空!");
}
Authentication authentication = null;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 该方法会去调用UserDetailsService.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
throw new RuntimeException("用户名和密码不匹配");
} else {
throw new RuntimeException(e.getMessage());
}
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//生成token
return tokenService.createToken(loginUser);
}
}
9、Security
9.1、OncePerRequestFilter(JWT令牌解析)
JwtAuthenticationTokenFilter实现了OncePerRequestFilter接口,所以每个请求都会被调用一次。在这里获取token后,进行解析和校验,然后将认证信息放入到Security上下文环境中。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
9.2、FilterInvocationSecurityMetadataSource(自定义URL匹配)
CustomFilterInvocationSecurityMetadataSource实现了FilterInvocationSecurityMetadataSource。在认证成功之后,会回调getAttributes()方法,以获取本次请求所需的角色信息。
我们在这里自定义自己的url匹配逻辑:
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
//用于实现ant风格的URL匹配
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private SysPermissionService sysPermissionService;
//Collection<ConfigAttribute>表示当前请求URL所需的角色
//Spring Security中通过FilterInvocationSecurityMetadataSource接口中的getAttributes方法来确定一个请求需要哪些角色
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取当前请求的URL
String requestUrl = ((FilterInvocation) o).getRequestUrl();
// 忽略url请放在此处进行过滤放行
if (requestUrl.contains("/login") || requestUrl.contains("/register")) {
return null;
}
//获取数据库中的资源信息,一般放在Redis缓存中
List<SysPermission> permissions = sysPermissionService.listAll();
//遍历信息,遍历过程中获取当前请求的URL所需要的角色信息并返回
for (SysPermission permission : permissions){
//匹配
if(antPathMatcher.match(permission.getUrl(), requestUrl)){
//获取所需的角色
List<SysRole> roles = sysPermissionService.getRolesByPermissionId(permission.getId());
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++){
roleArr[i] = roles.get(i).getCode();
}
return SecurityConfig.createList(roleArr);
}
}
//如果当前请求的URL在资源表中不存在响应的模式,就假设该请求登录后即可访问,直接返回ROLE_LOGIN
return SecurityConfig.createList("ROLE_LOGIN");
}
//getAllConfigAttributes()方法用来返回所有定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确
//如果不需要校验,直接返回null即可
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
//supports方法返回类对象是否支持校验
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
9.3、AccessDecisionManager(自定义鉴权)
CustomAccessDecisionManager实现了AccessDecisionManager接口。在FilterInvocationSecurityMetadataSource.getAttributes()
后,就会调用decide()
方法来判断当前登录的用户是否具有当前请求URL所需要的角色信息。
我们在这里实现自定义角色逻辑:
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
//在该方法中判断当前登录的用户是否具有当前请求URL所需要的角色信息,如果不具备,就抛出AccessDeniedException异常,否则不做任何事情
/*
参数:
1. Authentication: 包含当前登录用户的信息
2. Object: 是一个FilterInvocation对象,可以获取当前请求对象
3. Collection<ConfigAttribute>: FilterInvocationSecurityMetadataSource中的getAttributes方法的返回值,即当前请求URL所需要的角色
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//当前用户所具有的角色
Collection<? extends GrantedAuthority> auths = authentication.getAuthorities();
//当前url所需要的角色
for (ConfigAttribute configAttribute : collection){
if("ROLE_LOGIN".equals(configAttribute.getAttribute())){
//如果角色是"ROLE_LOGIN",说明当前请求的URL用户登录后即可访问
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登录!");
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
throw new AccessDeniedException("未授权该url");
}
//如果auth是UsernamePasswordAuthenticationToken的实例,那么说明当前用户已登录,该方法到此结束,否则进入正常判断流程
// return;
}
//如果当前用户具备当前请求需的角色,方法结束。
for (GrantedAuthority authority : auths){
//只要匹配到一个即可
if (configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
9.4、AuthenticationEntryPoint(认证失败处理)
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(AjaxResult.error(code, msg)));
}
}
9.5、AccessDeniedHandler(鉴权失败处理)
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setStatus(403);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(AjaxResult.error(e.getMessage())));
}
}
9.6、LogoutSuccessHandler(注销登录处理)
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Autowired
private TokenService tokenService;
/**
* 退出处理
*
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (ObjectUtil.isNotNull(loginUser)) {
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getUser().getId());
}
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSONUtil.toJsonStr(AjaxResult.success("退出成功")));
}
}
10、测试
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public AjaxResult login(@RequestParam("username") String username, @RequestParam("password")String password) {
//生成令牌
String token = loginService.login(username, password);
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 用户---角色sspring---权限
* admin:
* admin:
* /admin/**
* /test/**
* /a/**
* /visitor/**
* acton:
* visitor:
* /visitor/**
* /a/**
* test:
* /test/**
*
* test:
* test:
* /test/**
*/
@GetMapping("/admin/test")
public AjaxResult admin() {
return AjaxResult.success("/admin/**");
}
@GetMapping("/test/test")
public AjaxResult test() {
return AjaxResult.success("/test/**");
}
@GetMapping("/a/test")
public AjaxResult a() {
return AjaxResult.success("/a/**");
}
@GetMapping("/visitor/test")
public AjaxResult visitor() {
return AjaxResult.success("/visitor/**");
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encode = encoder.encode("123456");
System.out.println(encode);
}
}
10.1、admin用户测试
10.2、acton用户测试