接入数据源
配置redis
1、pom依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.13</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
2、redis配置类
RedisConfig
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
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.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
3、redis工具类
RedisCache
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@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 缓存键值
* @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 long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存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
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(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);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
4、application.yml配置
添加redis的相关连接配置
spring:
datasource:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: 10000ms
max-idle: 8
min-idle: 5
配置mysql
1、pom依赖包
引入MybatisPuls、mysql驱动
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
2、application.yml配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/share?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
3、创建数据库
CREATE TABLE `s_sys_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`account` varchar(64) NOT NULL DEFAULT '' COMMENT '账号',
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
`mobile` varchar(64) NOT NULL DEFAULT '' COMMENT '电话',
`nick_name` varchar(64) NOT NULL DEFAULT '' COMMENT '昵称',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
`update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人',
`remark_` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
4、创建User实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName(value = "s_sys_user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "account")
private String account;
@TableField(value = "password")
private String password;
@TableField(value = "mobile")
private String mobile;
@TableField(value = "nick_name")
private String nickName;
@TableField(value = "nick_name")
private Integer status;
}
5、创建Mapper接口文件
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cys.share.user.entiy.SysUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<SysUser> {
}
6、配置Mapper扫描
在启动类添加注解@MapperScan(“com.cys.share.user.mapper”)
@SpringBootApplication
@MapperScan("com.cys.share.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
集成security
1、认证流程实现
步骤一、添加pom依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.13</version>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.14.1</version>
</dependency>
步骤二、新增登录授权类LoginUser
import com.cys.share.user.entiy.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getAccount();
}
@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.LambdaQueryWrapper;
import com.cys.share.user.dto.LoginUser;
import com.cys.share.user.entiy.SysUser;
import com.cys.share.user.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount, username);
SysUser user = userMapper.selectOne(queryWrapper);
// 如果没有查询到用户则抛出异常,在过滤链中有异常捕获,这里抛出的异常会被捕获
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO 查询对应的权限信息
// 把数据封装成UserDetails返回
return new LoginUser(user);
}
}
步骤四、新增配置类
import com.cys.share.user.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.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.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许访问
.antMatchers("/login").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
步骤五、创建登录接口
这里创建两个接口,一个登录接口login,一个常规接口test。test接口主要是用做后续进行测试
@RestController
public class UserAuthController {
@Autowired
private UserAuthService userAuthService;
@PostMapping("/login")
public Result login(@RequestBody UserAuthDto userAuthDto) {
return userAuthService.login(userAuthDto);
}
@PostMapping("/test")
public Result test() {
return Result.success("test成功");
}
}
步骤六、登录功能的实现
import com.cys.share.user.dto.LoginUser;
import com.cys.share.user.dto.UserAuthDto;
import com.cys.share.user.service.UserAuthService;
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.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class UserAuthServiceImpl implements UserAuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public Result login(UserAuthDto user) {
// 认证的时候需要Authentication对象,所以需Au要一个Authentication的实现类,这里选择了UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getAccount(),user.getPassword());
// AuthenticationManager authenticate方法进行认证。在SecurityConfig配置类中,我们将AuthenticationManager注入到容器中。
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证通过,authenticate里将包含principal属性,该属性的值就是LoginUser,
// 如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String id = loginUser.getSysUser().getId().toString();
String jwt = JwtUtil.createJWT(id);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
// 把完整的用户信息存入redis,userid作为key
redisCache.setCacheObject("login:" + id, loginUser);
return Result.success(map );
}
}
步骤七、新增jwt认证过滤器
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
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.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 {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行,后面还有其他过滤器
filterChain.doFilter(request, response);
// 所有过滤器执行完毕后,响应回来还会走到这里
return;
}
// 解析token
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String redisKey = "login:" + id;
Object cacheObject = redisCache.getCacheObject(redisKey);
LoginUser loginUser = JSON.parseObject(JSON.toJSONString(cacheObject),LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 后面将需要一个Authentication的对象,在这里通过实现类UsernamePasswordAuthenticationToken构造这个对象
// 选择3个参数的构造器,principal:账号,credentials:密码,authorities:权限
// 为什么要选择这个构造器呢?因为这个构造器中有,super.setAuthenticated(true); 标识用户为已认证。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
// 存入SecurityContextHolder
// 存入需要一个Authentication的对象,在登录的时候也用到过类似的方法。
// TODO 获取权限信息封装到Authentication
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
步骤八、SecurityConfig添加过滤器配置
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
步骤九、测试登录
实际项目中我们不会把密码明文存储在数据库中,SpringSecurity为我们提供的BCryptPasswordEncoder工具进行加密。现在我们需要做两件事,
第一件:测试BCryptPasswordEncoder的加密功能
第二件:将密码加密后手动存到数据库中,这样我们才能验证登录功能
测试BCryptPasswordEncoder的加密功能
@Test
public void test(){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("12345678");
//这里输出:$2a$10$6GpCxT.ZndZQFWmfCVc0gO0d7lEEtDL6fLIEAHRcaU8E787WPdXo6
System.out.println(encode);
}
手动将加密后的密码存到数据库
进行登录测试,登录成功返回token
进行其他接口测试
前面我们还创建了一个test接口,现在进行调用测试,调用时headers中需要添加登录返回的token,不然会失败,测试结果如下
步骤十、实现登出接口
controller添加接口:
@GetMapping("/loginOut")
public Result loginOut() {
return userAuthService.logout();
}
userAuthService添加登出实现:
@Override
public Result logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long id = loginUser.getSysUser().getId();
// 删除redis当中的值
redisCache.deleteObject("login:" + id);
return Result.success("","注销成功");
}
测试退出功能
调用loginOut接口,退出成功。再次调用test接口则失败
2、授权流程实现
步骤一、创建相关的权限控制表
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。其中包括五张表,分别是用户表(前面我们已经创建)、菜单权限表、角色表、角色和菜单权限之间的关系以及用户和角色之间的关系表。
CREATE TABLE `s_sys_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`account` varchar(64) NOT NULL DEFAULT '' COMMENT '账号',
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
`mobile` varchar(64) NOT NULL DEFAULT '' COMMENT '电话',
`nick_name` varchar(64) NOT NULL DEFAULT '' COMMENT '昵称',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
`update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人',
`remark_` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
CREATE TABLE `s_sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '账号',
`role_key` varchar(512) DEFAULT NULL COMMENT '角色权限字符串',
`status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1正常 0停用)',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_by` bigint(200) DEFAULT NULL,
`remark_` varchar(500) DEFAULT NULL COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `s_sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT '' COMMENT '权限菜单名',
`path` varchar(255) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`is_view` tinyint(1) DEFAULT 1 COMMENT '菜单状态(1显示 0隐藏)',
`status` tinyint(1) DEFAULT 1 COMMENT '菜单状态(1正常 0停用)',
`perms` varchar(128) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(128) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_by` bigint(200) DEFAULT NULL,
`remark_` varchar(500) DEFAULT NULL COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='权限菜单表';
CREATE TABLE `s_sys_role_menu` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '菜单id',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_by` bigint(200) DEFAULT NULL,
`remark_` varchar(500) DEFAULT NULL COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='角色与菜单权限关系表';
CREATE TABLE `s_sys_user_role` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '角色id',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_by` bigint(200) DEFAULT NULL,
`remark_` varchar(500) DEFAULT NULL COMMENT '备注',
`isvalid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否有效 0否,1是',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户与角色关系表';
步骤二、创建相关的项目文件
根据表结构,这里需要创建相应的实体类和mapper文件
实体类
@Data
@TableName(value = "s_sys_menu")
public class SysMenu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "menu_name")
private String menuName;
@TableField(value = "path")
private String path;
@TableField(value = "component")
private String component;
@TableField(value = "is_view")
private Integer isView;
@TableField(value = "status")
private Integer status;
@TableField(value = "perms")
private String perms;
@TableField(value = "icon")
private String icon;
}
@Data
@TableName(value = "s_sys_role")
public class SysRole implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "name")
private String name;
@TableField(value = "role_key")
private String roleKey;
@TableField(value = "status")
private Integer status;
}
@Data
@TableName(value = "s_sys_role_menu")
public class SysRoleMenu implements Serializable {
private static final long serialVersionUID = 1L;
@TableField(value = "role_id")
private Long roleId;
@TableField(value = "menu_id")
private Long menuId;
}
@Data
@TableName(value = "s_sys_user_role")
public class SysUserRole implements Serializable {
private static final long serialVersionUID = 1L;
@TableField(value = "user_id")
private Long userId;
@TableField(value = "role_id")
private Long roleId;
}
mapper接口文件
@Mapper
public interface MenuMapper extends BaseMapper<SysMenu> {
/**
* 根据用户id获取权限
* @param userId 用户id
* @return
*/
List<String> listPermsByUserId(Long userId);
}
@Mapper
public interface RoleMapper extends BaseMapper<SysRole> {
}
@Mapper
public interface RoleMenuMapper extends BaseMapper<SysRoleMenu> {
}
@Mapper
public interface UserRoleMapper extends BaseMapper<SysUserRole> {
}
mapper实现文件
由于MenuMapper添加了一个查询权限的方法,需要在xml文件内实现,新增MenuMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cys.share.user.mapper.MenuMapper">
<select id="listPermsByUserId" resultType="java.lang.String" parameterType="java.lang.Long">
select distinct m.perms
from s_sys_user_role ur
left join s_sys_role r on ur.role_id = r.id
left join s_sys_role_menu rm on ur.role_id = rm.role_id
left join s_sys_menu m on m.id = rm.menu_id
where user_id = #{id}
and r.status = 1
and m.status = 1
</select>
</mapper>
步骤三、在application.yml中配置mapperXML文件的位置
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
步骤四、开启相关配置
在配置文件SecurityConfig上面加上注解@EnableGlobalMethodSecurity, 表示开启资源访问控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
在相应的资源上添加资源配置,比如上面创建的test接口,添加@PreAuthorize注解
@PreAuthorize("hasAuthority('test')")
@GetMapping("/test")
public Result test() {
return Result.success("test成功");
}
步骤五、封装权限信息
之前我们创建的LoginUser中,getAuthorities方法是返回空的,下面是增加返回权限的全量内容
import com.alibaba.fastjson.annotation.JSONField;
import com.cys.share.user.entiy.SysUser;
import lombok.AllArgsConstructor;
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 java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
private List<String> permissions;
public LoginUser(SysUser sysUser, List<String> permissions) {
this.sysUser = sysUser;
this.permissions = permissions;
}
/**
* Redis默认时不会把它进行序列,但是这样会出问题
* 其实,我们不需要把这个成员变量序列号存储到Redis当中,我们只需要存储permissions即可。
* 我们可以将permissions转换为authorities。
* 通过 @JSONField(serialize = false) 注解,可以不让它序列号。
*/
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
// 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getAccount();
}
@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添加查询权限
添加了 List list = menuMapper.listPermsByUserId(user.getId());
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount, username);
SysUser user = userMapper.selectOne(queryWrapper);
// 如果没有查询到用户则抛出异常,在过滤链中有异常捕获,这里抛出的异常会被捕获
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO 查询对应的权限信息
// 查询对应的权限信息
List<String> list = menuMapper.listPermsByUserId(user.getId());
// 把数据封装成UserDetails返回
return new LoginUser(user,list);
}
}
步骤七、自定义失败处理
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是无该权限导致的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
认证失败处理
新增AuthenticationEntryPointImpl文件,返回状态码 401
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String json = JSON.toJSONString(Result.failure(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录"));
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(json);
}
}
无权限失败处理
新增AccessDeniedHandlerImpl 文件,返回状态码 403
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String json = JSON.toJSONString(Result.failure(HttpStatus.FORBIDDEN.value(), "您的权限不足"));
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(json);
}
}
步骤八、修改配置类SecurityConfig
步骤七新增了两个异常处理器,需要子配置类中添加
// 配置异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 认证失败处理
*/
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
/**
* 无权限失败处理
*/
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许访问
.antMatchers("/login").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
步骤九、修改JwtAuthenticationTokenFilter文件
校验的时候,也要获取权限。添加如下
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行,后面还有其他过滤器
filterChain.doFilter(request, response);
// 所有过滤器执行完毕后,响应回来还会走到这里
return;
}
// 解析token
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String redisKey = "login:" + id;
Object cacheObject = redisCache.getCacheObject(redisKey);
LoginUser loginUser = JSON.parseObject(JSON.toJSONString(cacheObject),LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 后面将需要一个Authentication的对象,在这里通过实现类UsernamePasswordAuthenticationToken构造这个对象
// 选择3个参数的构造器,principal:账号,credentials:密码,authorities:权限
// 为什么要选择这个构造器呢?因为这个构造器中有,super.setAuthenticated(true); 标识用户为已认证。
// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// 存入SecurityContextHolder
// 存入需要一个Authentication的对象,在登录的时候也用到过类似的方法。
// TODO 获取权限信息封装到Authentication
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
步骤十、授权测试
首先需要添加数据给表角色表s_sys_role
设置一个菜单资源test,插入到表s_sys_menu
绑定体验角色拥有菜单资源test的权限
再回顾一下用户信息,现在暂时不要给用户分配角色
开始进行测试
首先进行登录,登录成功
再拿该token去访问test接口,提示权限不足,和预期的结果一样
此时给用户分配对应的体验角色,拥有test资源的权限
然后重新进行测试,切记,需要重新登录获取新的token,才拥有新权限。测试结果如下,