简介
Spring Security 是 Spring 家族中安全管理框架,相比于 Shiro ,它提供了更丰富的功能,社区资源比 Shiro 丰富。 一般大部分中大型项目都是使用 Security ,小项目使用 Shiro 比较多。因为相对于 Security,Shiro 上手更简单。 一般Web应用的需要进行认证和授权。 认证:验证是否是本系统用户。 授权:经过登录,判断当前用户拥有的权限。
一、快速入门
- pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- controller
@RestController
public class AuthController {
@PostMapping("/hello")
public String hello() {
return "Hello Security";
}
}
- 启动
 - 测试
账号:user 密码:项目启动时随机生成  
二、 初探原理
登陆校验流程(前后端分离)

SpringSecurity完整流程
SpringSecurity的流程其实就是一个过滤链,内包含了多个过滤器。  图中只展示核心过滤器。 UsernamePasswordAuthenticationFilter:负责处理账号密码登录请求。 ExceptionTranslationFilter: 处理过滤器中抛出的任何AccessDeniedException和AuthenticationException。 FilterSecurityinterceptor:负责处理权限校验的过滤器。
完整过滤器链

三、认证
思路
登录:  校验: 
准备工作
数据表
① mysql 用户数据表
CREATE TABLE `market_user` (
`user_id` bigint(0) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐',
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
`status` tinyint(0) NULL DEFAULT NULL COMMENT '状态 0:禁用 1:正常',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1406806069329657858 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户' ROW_FORMAT = Dynamic;
INSERT INTO `market_user` VALUES (1, 'admin', '$2a$10$nZ5WisHs6gTGFCHNb0iAfum.QaAiOtcXDpLEX.C2/umbPXUuLuuIC', 'i2I9Lr5MKetiO1Zk7IpC', 'root@renren.io', '13612345678', 1, '2016-11-11 11:11:11');

maven
② xml 依赖配置
<properties>
<java.version>1.8</java.version>
<druid.version>1.2.6</druid.version>
<mysql.version>8.0.22</mysql.version>
<plus.version>3.4.1</plus.version>
<swagger.version>2.9.2</swagger.version>
<druid.version>1.2.6</druid.version>
<swaggerio.version>1.5.21</swaggerio.version>
<plus-generator.version>3.4.1</plus-generator.version>
<swagger-bootstrap>1.9.6</swagger-bootstrap>
<hutool.varsion>5.4.5</hutool.varsion>
<jjwt.version>0.9.0</jjwt.version>
<java-jwt.version>3.2.0</java-jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${plus.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>${swagger-bootstrap}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.varsion}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
yml 配置
③ application.yml 配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/market?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
type-aliases-package: com.example.securitydemo.entity
mapper-locations: classpath*:/mapper
swagger配置
④ swaggerConfig Swagger API配置
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {
@Bean
@ConditionalOnMissingBean
public SwaggerProperties swaggerProperties(){
return new SwaggerProperties();
}
@Bean
public Docket createRestApi(SwaggerProperties swaggerProperties){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo(swaggerProperties))
.enable(true)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.securitydemo.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {
Contact contact = new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(),swaggerProperties.getContactEmail());
return new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
.contact(contact)
.version(swaggerProperties.getVersion())
.build();
}
}
⑤ Swagger yml映射
@Data
@ConfigurationProperties("lanys.swagger")
public class SwaggerProperties {
private String title;
private String description;
private String termsOfServiceUrl;
private String ContactName;
private String ContactUrl;
private String ContactEmail;
private String version;
}
Redis 配置
⑥ RedisConfig Redis配置
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
public class RedisConstants {
public static final Long TOKEN_OVERDUE_TIME = 86400L;
public static final String USER_CATALOG = "market:user:{}";
}
@Slf4j
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public boolean set(final String key,Object value,Long expireTime) {
boolean result = false;
try {
String toJSONString = JSON.toJSONString(value);
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key,toJSONString,expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public void remove(final String... keys){
log.info("redis 删除多个数据->{}", (Object) keys);
for (String key: keys) {
redisTemplate.delete(key);
}
}
public void remove(final String key) {
log.info("redis 删除单个数据->{}",key);
if (exists(key)) {
redisTemplate.delete(key);
}
}
public boolean exists(final String key){
try{
log.info("redis 校验数据是否存在->{}",key);
Boolean result = redisTemplate.hasKey(key);
if (result != null) {
return result;
}
}catch (Exception e) {
log.error("redis 校验数据是否存在数据异常:",e);
}
return false;
}
public Object get(final String key) {
Object data = null;
try {
log.info("获取Redis数据->{}",key);
ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();
data = valueOperations.get(key);
}catch (Exception e) {
log.error("获取Redis数据数据异常:", e);
}
return data;
}
public boolean addHash(String keyOne,String keyTwo,Object value) {
try {
log.info("hash存储目录->{}下的key->{},value->{}",keyOne,keyTwo,value);
redisTemplate.opsForHash().put(keyOne,keyTwo,value);
return true;
}catch (Exception e) {
log.error("hash存储类型数据异常:",e);
}
return false;
}
public boolean addHashMap(String key, Map<String,Object> map) {
try {
log.info("存储hash类型key->{},value->{}",key,map.toString());
redisTemplate.opsForHash().putAll(key,map);
return true;
}catch (Exception e) {
log.error("存储hash类型数据异常:",e);
}
return false;
}
public Object getMapString(String keyOne,String keyTwo) {
try{
log.info("获取hash目录->{}下的key->{}",keyOne,keyTwo);
Object result = redisTemplate.opsForHash().get(keyOne, keyTwo);
if (result != null) {
return result;
}
}catch (Exception e){
log.error("获取hash目录下的key数据异常:",e);
}
return null;
}
}
Token工具
⑦ TokenUtil token工具
@Slf4j
public class TokenUtil {
public final static String jwtTokenSecret="MARKET";
public static String doGenerateToken(String key) {
final Date createdDate = new Date();
final Date finishDate = new Date(createdDate.getTime() + 86400);
return Jwts.builder()
.setSubject(key)
.setIssuedAt(createdDate)
.signWith(SignatureAlgorithm.HS256, jwtTokenSecret)
.compact();
}
public static Claims parsingToken(String token) {
return Jwts.parser().setSigningKey(jwtTokenSecret) .parseClaimsJws(token)
.getBody();
}
}
统一返回封装
⑧ Result 返回封装
@ApiModel(value = "返回值基本信息")
@Data
public class Result {
@ApiModelProperty(value = "状态")
private int code;
@ApiModelProperty(value = "内容")
private Object data;
@ApiModelProperty(value = "描述")
private String msg;
public Result(int code, String msg, Object data) {
this.code = code;
this.data = data;
this.msg = msg;
}
public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static Result success() {
return new Result(ResultEnums.SUCCESS.getCode(), ResultEnums.SUCCESS.getMsg());
}
public static Result success(String msg) {
return new Result(ResultEnums.SUCCESS.getCode(), ResultEnums.SUCCESS.getMsg(), msg);
}
public static Result fail() {
return new Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg());
}
public static Result fail(Object data) {
return new Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg(), data);
}
public static Result exception() {
return new Result(ResultEnums.EXCEPTION.getCode(), ResultEnums.EXCEPTION.getMsg());
}
public static Result exception(Object data) {
return new Result(ResultEnums.EXCEPTION.getCode(), ResultEnums.EXCEPTION.getMsg(), data);
}
public Result put(Object data) {
this.data = data;
return this;
}
}
⑨ 枚举
@Getter
public enum ResultEnums {
SUCCESS(200, "成功"),
FAIL(500, "异常"),
EXCEPTION(500, "失败");
private int code;
private String msg;
ResultEnums(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
Security 核心配置
⑩ SecurityConfig Security核心配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
System.out.println("加载加密配置");
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login","/doc.html","/swagger/**","/v2/api-docs"
,"/swagger-ui.html","/swagger-resources/**","/webjars/**","/logout").anonymous()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
⑩① UserDetailsServiceImpl 认证Service
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private IMarketUserService marketUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
LambdaQueryWrapper<MarketUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(MarketUser::getUsername,username);
MarketUser marketUser = this.marketUserService.getOne(lambdaQueryWrapper);
if (marketUser == null) {
throw new RuntimeException("用户或密码错误");
}
log.info("[Security查询用户信息]参数:-> {}",marketUser.toString());
return new LoginUser(marketUser);
}catch (Exception e) {
log.error("Security校验账号密码异常:",e);
}
return null;
}
}
⑩② LoginUser 重写Security 用户详情
@Setter
@Getter
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {
private MarketUser marketUser;
public LoginUser(MarketUser marketUser) {
this.marketUser = marketUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.marketUser.getPassword();
}
@Override
public String getUsername() {
return this.marketUser.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;
}
}
⑩③ JwtAuthenticationTokenFilter token过滤器
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
String authorization = httpServletRequest.getHeader("Authorization");
if (StrUtil.isBlank(authorization)) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
};
log.info("过滤器校验token:{}",authorization);
Claims claims = TokenUtil.parsingToken(authorization);
String subject = claims.getSubject();
if (StrUtil.isNotBlank(subject)) {
Object obj = redisUtils.get(StrUtil.format(RedisConstants.USER_CATALOG,subject));
LoginUser loginUser = null;
if (obj != null) {
String toJSONString = JSON.toJSONString(obj);
Object parse = JSON.parse(toJSONString);
loginUser = JSONObject.parseObject(parse.toString(), LoginUser.class);
}
if (loginUser == null) {
throw new RuntimeException("用户未登录");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}catch (Exception e) {
log.error("过滤器校验token异常:",e);
}
}
}
三层
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("market_user")
public class MarketUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "user_id", type = IdType.AUTO)
private Long userId;
@TableField("username")
private String username;
@TableField("password")
private String password;
@TableField("salt")
private String salt;
@TableField("email")
private String email;
@TableField("mobile")
private String mobile;
@TableField("status")
private Integer status;
@TableField("create_time")
private LocalDateTime createTime;
}
public interface IMarketUserService extends IService<MarketUser> {}
@Service
public class MarketUserServiceImpl extends ServiceImpl<MarketUserMapper, MarketUser> implements IMarketUserService {}
public interface IMarketUserService extends IService<MarketUser> {}
@RestController
public class AuthController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisUtils redisUtils;
@ApiOperation(value = "登录模块")
@PostMapping("/login")
@ApiImplicitParams({
@ApiImplicitParam(name = "userName", value = "账号", paramType = "query", example = "username"),
@ApiImplicitParam(name = "password", value = "密码", paramType = "query", example = "password")
})
public Result login(@NotBlank(message = "账号不能为空") String userName,
@NotBlank(message = "密码不能为空") String password) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName,password);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (authenticate == null) {
return Result.fail("账号或密码异常");
}
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
HashMap<String, Object> hashMap = new HashMap<>(2);
if (loginUser.getMarketUser() != null) {
Long userId = loginUser.getMarketUser().getUserId();
redisUtils.set(StrUtil.format(RedisConstants.USER_CATALOG,userId),loginUser, RedisConstants.TOKEN_OVERDUE_TIME);
hashMap.put("token", TokenUtil.doGenerateToken(userId.toString()));
}
return Result.success("登录成功").put(hashMap);
}
@ApiOperation("註銷")
@PatchMapping("/logout")
public Result logout () {
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
Long userId = loginUser.getMarketUser().getUserId();
redisUtils.remove(StrUtil.format(RedisConstants.USER_CATALOG,userId));
return Result.success();
}
@GetMapping("/hello")
public String hello() {
return "Hello Security";
}
}
测试
  
四、授权
正常情况下,在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。 ? 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。 ? 然后设置我们的资源所需要的权限即可。
授权使用
要使用它我们需要先开启相关配置 SecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解。@PreAuthorize sys:schedule:list,sys:schedule:info 指权限
@PreAuthorize("hasAnyAuthority('sys:schedule:list,sys:schedule:info')")
@GetMapping("/hello")
public String hello() {
return "Hello Security";
}
准备工作
① mysql
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` bigint NOT NULL AUTO_INCREMENT,
`parent_id` bigint DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单名称',
`url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`type` int DEFAULT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单图标',
`order_num` int DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单管理';
insert into `sys_menu`(`menu_id`,`parent_id`,`name`,`url`,`perms`,`type`,`icon`,`order_num`) values
(1,0,'系统管理',NULL,NULL,0,'fa fa-cog',0),
(2,1,'管理员管理','modules/sys/user.html',NULL,1,'fa fa-user',1),
(3,1,'角色管理','modules/sys/role.html',NULL,1,'fa fa-user-secret',2),
(4,1,'菜单管理','modules/sys/menu.html',NULL,1,'fa fa-th-list',3),
(5,1,'SQL监控','druid/sql.html',NULL,1,'fa fa-bug',4),
(6,1,'定时任务','modules/job/schedule.html',NULL,1,'fa fa-tasks',5),
(7,6,'查看',NULL,'sys:schedule:list,sys:schedule:info',2,NULL,0),
(8,6,'新增',NULL,'sys:schedule:save',2,NULL,0),
(9,6,'修改',NULL,'sys:schedule:update',2,NULL,0),
(10,6,'删除',NULL,'sys:schedule:delete',2,NULL,0),
(11,6,'暂停',NULL,'sys:schedule:pause',2,NULL,0),
(12,6,'恢复',NULL,'sys:schedule:resume',2,NULL,0),
(13,6,'立即执行',NULL,'sys:schedule:run',2,NULL,0),
(14,6,'日志列表',NULL,'sys:schedule:log',2,NULL,0),
(15,2,'查看',NULL,'sys:user:list,sys:user:info',2,NULL,0),
(16,2,'新增',NULL,'sys:user:save,sys:role:select',2,NULL,0),
(17,2,'修改',NULL,'sys:user:update,sys:role:select',2,NULL,0),
(18,2,'删除',NULL,'sys:user:delete',2,NULL,0),
(19,3,'查看',NULL,'sys:role:list,sys:role:info',2,NULL,0),
(20,3,'新增',NULL,'sys:role:save,sys:menu:perms',2,NULL,0),
(21,3,'修改',NULL,'sys:role:update,sys:menu:perms',2,NULL,0),
(22,3,'删除',NULL,'sys:role:delete',2,NULL,0),
(23,4,'查看',NULL,'sys:menu:list,sys:menu:info',2,NULL,0),
(24,4,'新增',NULL,'sys:menu:save,sys:menu:select',2,NULL,0),
(25,4,'修改',NULL,'sys:menu:update,sys:menu:select',2,NULL,0),
(26,4,'删除',NULL,'sys:menu:delete',2,NULL,0),
(27,1,'参数管理','modules/sys/config.html','sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete',1,'fa fa-sun-o',6),
(29,1,'系统日志','modules/sys/log.html','sys:log:list',1,'fa fa-file-text-o',7),
(30,1,'文件上传','modules/oss/oss.html','sys:oss:all',1,'fa fa-file-image-o',6),
(31,1,'部门管理','modules/sys/dept.html',NULL,1,'fa fa-file-code-o',1),
(32,31,'查看',NULL,'sys:dept:list,sys:dept:info',2,NULL,0),
(33,31,'新增',NULL,'sys:dept:save,sys:dept:select',2,NULL,0),
(34,31,'修改',NULL,'sys:dept:update,sys:dept:select',2,NULL,0),
(35,31,'删除',NULL,'sys:dept:delete',2,NULL,0),
(36,1,'字典管理','modules/sys/dict.html',NULL,1,'fa fa-bookmark-o',6),
(37,36,'查看',NULL,'sys:dict:list,sys:dict:info',2,NULL,6),
(38,36,'新增',NULL,'sys:dict:save',2,NULL,6),
(39,36,'修改',NULL,'sys:dict:update',2,NULL,6),
(40,36,'删除',NULL,'sys:dict:delete',2,NULL,6);
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色名称',
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色';
insert into `sys_role`(`id`,`role_name`,`user_id`,`create_time`) values
(1,'admin',1,'2022-09-22 23:10:06');
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`role_id` bigint DEFAULT NULL COMMENT '角色ID',
`menu_id` bigint DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
insert into `sys_role_menu`(`id`,`role_id`,`menu_id`) values
(1,1,7),
(2,1,8);
三层
② IMarketUserService 权限 Service
public interface IMarketMenuService extends IService<MarketMenu> {
List<String> findMenuByUserId(Long userId);
}
③ MarketUserServiceImpl 权限 ServiceImpl
@Service
public class MarketMenuServiceImpl extends ServiceImpl<MarketMenuMapper,MarketMenu> implements IMarketMenuService {
@Override
public List<String> findMenuByUserId(Long userId) {
return this.baseMapper.findMenuByUserId(userId);
}
}
③ MarketMenuMapper权限 Mapper
public interface MarketMenuMapper extends BaseMapper<MarketMenu> {
List<String> findMenuByUserId(@Param("userId") Long userId);
}
④ MarketMenuMapperXML权限 MapperXML
<?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.example.securitydemo.mapper.MarketMenuMapper">
<select id="findMenuByUserId" resultType="string">
select menu.perms from market_user AS user
LEFT JOIN sys_role AS role ON user.user_id = role.user_id
LEFT JOIN sys_role_menu AS role_menu ON role_id = role_menu.role_id
LEFT JOIN sys_menu AS menu ON role_menu.menu_id = menu.menu_id
WHERE user.user_id = #{userId}
</select>
</mapper>
完善Sercurity 配置
⑤ SecurityConfig 添加注解 @EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
System.out.println("加载加密配置");
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login","/doc.html","/swagger/**","/v2/api-docs"
,"/swagger-ui.html","/swagger-resources/**","/webjars/**","/logout").anonymous()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
⑥ UserDetailsServiceImpl 认证过程中添加权限
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private IMarketUserService marketUserService;
@Resource
private IMarketMenuService marketMenuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
LambdaQueryWrapper<MarketUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(MarketUser::getUsername,username);
MarketUser marketUser = this.marketUserService.getOne(lambdaQueryWrapper);
if (marketUser == null) {
throw new RuntimeException("用户或密码错误");
}
log.info("[Security查询用户信息]参数:-> {}",marketUser.toString());
List<String> strings = this.marketMenuService.findMenuByUserId(marketUser.getUserId());
return new LoginUser(marketUser,strings);
}catch (Exception e) {
log.error("Security校验账号密码异常:",e);
}
return null;
}
}
⑦ LoginUser 重载Security用户信息类保存权限
@Setter
@Getter
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {
private MarketUser marketUser;
private List<String> permissions;
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(MarketUser marketUser) {
this.marketUser = marketUser;
}
public LoginUser(MarketUser marketUser, List<String> permissions) {
this.marketUser = marketUser;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (!CollectionUtils.isEmpty(authorities)) {
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return this.marketUser.getPassword();
}
@Override
public String getUsername() {
return this.marketUser.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;
}
}
⑧ JwtAuthenticationTokenFilter token过滤器将验证token成功后,解析token获取权限,将权限保存在Security Authentication中
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
String authorization = httpServletRequest.getHeader("Authorization");
if (StrUtil.isBlank(authorization)) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
};
log.info("过滤器校验token:{}",authorization);
Claims claims = TokenUtil.parsingToken(authorization);
String subject = claims.getSubject();
if (StrUtil.isNotBlank(subject)) {
Object obj = redisUtils.get(StrUtil.format(RedisConstants.USER_CATALOG,subject));
LoginUser loginUser = null;
if (obj != null) {
String toJSONString = JSON.toJSONString(obj);
Object parse = JSON.parse(toJSONString);
loginUser = JSONObject.parseObject(parse.toString(), LoginUser.class);
}
if (loginUser == null) {
throw new RuntimeException("用户未登录");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}catch (Exception e) {
log.error("过滤器校验token异常:",e);
}
}
}
测试

  
|