简介
Spring Security 是 Spring 家族中安全管理框架,相比于 Shiro ,它提供了更丰富的功能,社区资源比 Shiro 丰富。
一般大部分中大型项目都是使用 Security ,小项目使用 Shiro 比较多。因为相对于 Security,Shiro 上手更简单。
一般Web应用的需要进行认证和授权。
认证:验证是否是本系统用户。
授权:经过登录,判断当前用户拥有的权限。
一、快速入门
- pom.xml
<!-- Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- controller
/**
* @Author: Lanys
* @Description: 测试接口
* @Date: Create in 22:03 2022/9/20
*/
@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>
<!-- redis -->
<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>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Mybatis-plus 依赖 -->
<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>
<!--jwt-->
<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/*.xml
configuration:
default-statement-timeout: 120
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
redis:
host: 8.134.130.51
port: 6379
password: 813413051
#swagger
lanys:
swagger:
title: material
description: material
termsOfServiceUrl: https://eurasia.plus/swagger-ui.html
ContactName: xxx
ContactUrl: https://eurasia.plus/swagger-ui.html
ContactEmail: 1090613735@qq.com
version: 1.0
swagger配置
④ swaggerConfig Swagger API配置
/**
* @Author: Lanys
* @Description:
* @Date: Create in 17:14 2021/3/22
*/
@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();
}
/**
* 创建该API的基本信息(这些基本信息会展现在文档页面中)
* 访问地址:http://项目实际地址/doc.html
*
* @return
*/
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映射
/**
* @ClassName SwaggerProperties
* @Author zwy
* @Data 1/4/2021 下午5:09
*/
@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配置
/**
* @author lanys
* @Description: Redis 配置
* @date 28/6/2021 上午10:43
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
/**
* redisTemplate模板
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 使用Jackson进行序列化
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;
}
}
/**
* @Author: Lanys
* @Description: Redis常量
* @Date: Create in 22:34 2022/5/23
*/
public class RedisConstants {
/**
* redis token time
*/
public static final Long TOKEN_OVERDUE_TIME = 86400L;
/**
* Redis User catalog
*/
public static final String USER_CATALOG = "market:user:{}";
}
/**
* @Author: Lanys
* @Description: redis 工具
* @Date: Create in 21:33 2022/5/22
*/
@Slf4j
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 写入缓存
* @param key
* @param value
* @return
*/
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;
}
/**
* 写入缓存设置时间
* @param key key
* @param value value
* @param expireTime 设置过期时间
* @return
*/
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;
}
/**
* 批量删除数据
* @param keys key
*/
public void remove(final String... keys){
log.info("redis 删除多个数据->{}", (Object) keys);
for (String key: keys) {
redisTemplate.delete(key);
}
}
/**
* 删除单个数据
* @param key key
*/
public void remove(final String key) {
log.info("redis 删除单个数据->{}",key);
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断当前key是否存储
* @param key key
* @return 是否存在
*/
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;
}
/**
* 获取单个 redis 数据
* @param key key
* @return
*/
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;
}
/**
* hash存储
* @param keyOne redis key
* @param keyTwo hash 存储key
* @param value hash 存储value
* @return
*/
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;
}
/**
* hash批量存储
* @param key key
* @param map map 指的是多个,就是批量
* @return boolean
*/
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;
}
/**
* 获取hash存储的数据
* @param keyOne key
* @param keyTwo key
* @return
*/
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工具
/**
* @ClassName TokenUtil
* @Author lanys
* @Data 1/4/2021 下午7:05
*/
@Slf4j
public class TokenUtil {
/** 盐 */
public final static String jwtTokenSecret="MARKET";
/**
* 生成token
* @param key 账号
* @return
*/
public static String doGenerateToken(String key) {
final Date createdDate = new Date();
//Constants.time * 1000
final Date finishDate = new Date(createdDate.getTime() + 86400);
return Jwts.builder()
.setSubject(key)
.setIssuedAt(createdDate)
// .setExpiration(finishDate)
.signWith(SignatureAlgorithm.HS256, jwtTokenSecret)
.compact();
}
/**
* 获取body
* @param token token
* @return
*/
public static Claims parsingToken(String token) {
return Jwts.parser().setSigningKey(jwtTokenSecret) .parseClaimsJws(token)
.getBody();
}
}
统一返回封装
⑧ Result 返回封装
/**
* @Author: Lanys
* @Description: 统一返回格式
* @Date: Create in 22:50 2021/11/5
*/
@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);
}
/**
* 无参返回值
*
* @return
*/
public static Result fail() {
return new Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg());
}
/**
* 错误有参返回值
*
* @return
*/
public static Result fail(Object data) {
return new Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg(), data);
}
/**
* 异常无参返回值
*
* @return
*/
public static Result exception() {
return new Result(ResultEnums.EXCEPTION.getCode(), ResultEnums.EXCEPTION.getMsg());
}
/**
* 异常有参返回值
*
* @return
*/
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;
}
}
⑨ 枚举
/**
* @Author: Lanys
* @Description: 统一返回格式泛型
* @Date: Create in 22:53 2021/11/5
*/
@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核心配置
/**
* @Author: Lanys
* @Description: Security 配置类
* @Date: Create in 23:35 2022/8/27
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 创建 BCryptPasswordEncoder 注入容器
* @return
*/
@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()
// 不通过Session 获取 SecurityContext
.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
/**
* @Author: Lanys
* @Description:
* @Date: Create in 22:35 2022/8/26
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private IMarketUserService marketUserService;
/**
* 校验账号密码
* @param username 账号
* @return 用户信息
* @throws UsernameNotFoundException
*/
@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 用户详情
/**
* @Author: Lanys
* @Description: 实现 UserDetails
* @Date: Create in 22:46 2022/8/26
*/
@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过滤器
/**
* @Author: Lanys
* @Description: security token过滤器
* @Date: Create in 23:02 2022/8/28
*/
@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");
// 没有token,放行
if (StrUtil.isBlank(authorization)) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
};
// 有token逻辑
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> {}
/**
* @Author: Lanys
* @Description: 测试接口
* @Date: Create in 22:03 2022/9/20
*/
@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) {
// AuthenticationManager 用户验证
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> {
/**
* 按用户 ID 查找菜单
* @param userId 用户id
* @return
*/
List<String> findMenuByUserId(Long userId);
}
③ MarketUserServiceImpl 权限 ServiceImpl
@Service
public class MarketMenuServiceImpl extends ServiceImpl<MarketMenuMapper,MarketMenu> implements IMarketMenuService {
/**
* 按用户 ID 查找菜单
* @param userId 用户id
* @return
*/
@Override
public List<String> findMenuByUserId(Long userId) {
return this.baseMapper.findMenuByUserId(userId);
}
}
③ MarketMenuMapper权限 Mapper
public interface MarketMenuMapper extends BaseMapper<MarketMenu> {
/**
* 按用户 ID 查找菜单
* @param userId 用户id
* @return
*/
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)
/**
* @Author: Lanys
* @Description: Security 配置类
* @Date: Create in 23:35 2022/8/27
*/
@Configuration
/** 开启权限信息 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 创建 BCryptPasswordEncoder 注入容器
* @return
*/
@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()
// 不通过Session 获取 SecurityContext
.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 认证过程中添加权限
/**
* @Author: Lanys
* @Description:
* @Date: Create in 22:35 2022/8/26
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private IMarketUserService marketUserService;
@Resource
private IMarketMenuService marketMenuService;
/**
* 校验账号密码
* @param username 账号
* @return 用户信息
* @throws UsernameNotFoundException
*/
@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用户信息类保存权限
/**
* @Author: Lanys
* @Description: 实现 UserDetails
* @Date: Create in 22:46 2022/8/26
*/
@Setter
@Getter
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {
private MarketUser marketUser;
/** 存储权限信息 */
private List<String> permissions;
/** 专门装权限的 List */
@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;
}
// permissions 中 String 类型的权限信息封装成 SimpleGrantedAuthority 对象
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中
/**
* @Author: Lanys
* @Description: security token过滤器
* @Date: Create in 23:02 2022/8/28
*/
@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");
// 没有token,放行
if (StrUtil.isBlank(authorization)) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
};
// 有token逻辑
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);
}
}
}
测试