Spring——security安全框架使用详解
1.前言
在日常开发中,几乎所有的项目都需要进行请求的安全校验操作。
通常会采取以下几种方式来实现安全校验 和过滤。
- 1.实例化HandlerInterceptor接口,配置其中的preHandle
、
postHandle、
afterCompletion 属性信息。 - 2、采取AOP的思想,手写一个接口的拦截过滤。
- 3、使用一些较为成熟的权限认证、校验框架。如:shiro、security等。
2.Security简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以多么容易地扩展以满足定制需求。
它是Spring家族中的一个安全管理框架。相比于另一个安全框架shiro,它提供了更丰富的功能,社区资源也比shiro丰富
官方文档地址
https://spring.io/projects/spring-security/
3.Security相关模块
-
核心模块:Spring-security-core.jar。包含核心验证和访问控制类接口,远程支持的基本配置API。任何使用Spring Security 的应用程序都需要这个模块。支持独立的应用程序、远程客户端、服务层方法安全和jdbc用户配置。
包含以下顶层包:
- org.springframework.security.core
- org.springframework.security.access
- org.springframework.security.authentication
- org.springframework.security.provisioning
-
远程调用:spring-security-remoting.jar。提供与 Spring Remoting 集成。
主要包为:org.springframework.security.remoting -
网页:spring-security-web.jar。包括网站安全的模块,提供网站认证服务和基于 URL 访问控制。
主包名为 org.springframework.security.web -
配置:spring-security-config.jar。包含安全命令空间的解析代码。如果你使用Spring Security XML命令空间进行配置你需要包含这个模块。 主包名为org.springframework.security.config
-
LDAP:spring-security-ldap.jar。LDAP验证和配置代码,如果你需要使用LDAP验证和管理LDAP用户实体,你需要这个模块。
主包名为 org.springframework.security.ldap -
ACL访问控制表:spring-security-acl.jar。ACL专门的领域对象的实现。用来在你的应用程序中应用安全特定的领域对象实例。
主包名为 org.springframework.security.acls -
CAS:spring-security-cas.jar。Spring Security的CAS客户端集成。如果你想用CAS的SSO服务器使用Spring Security网页验证需要该模块。
顶层的包是 org.springframework.security.cas -
OpenID:spring-security-openid.jar。OpenID 网页验证支持。使用外部的OpenID服务器验证用户。 org.springframework.security.openid. 需要 OpenID4Java
-
Test:spring-security-test.jar。支持Spring Security的测试。
4.快速入门
- 创建一个SpringBoot项目
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- springboot 快速启动的一些基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 导入SpringSecurity依赖
<!-- security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
版本号问题:一些SpringBoot整合了一些通用依赖的版本号,所以不需要自己手动控制依赖的版本号信息
- 创建一个Controller进行测试
运行项目会跳转到security提供的一个登录页面(用户名:user)密码在控制台中查看
密码(运行好项目后会打印在控制台):
更改用户名和密码,在application.yml中去设置security的username和password
Spring:
security:
user:
password: 123456
name: root
5.实际项目场景分析
- 实际项目中不会使用它默认的登录页(删除)
- 登录验证需要结合数据库进行校验
- 登录成功需要返回一个jwtToken
登录时
-
自定义登录接口
调用ProviderManager的方法进行认证,如果认证通过生成jwt
把用户信息存入redis中
-
自定义UserDetailService
在这个实现类中去查询数据库
校验
-
定义jwt认证过滤器
获取Token,解析Token,获取其中的key
通过key查询存入到redis中的用户信息
存入SecurityContentHolder
6.SpringSecurity完整流程
SpringSecurity的原理就是一个过滤器链,内部包含了各种功能的过滤器。它其中包含最主要的过滤器有以下这些:
-
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
-
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
-
FilterSecurityInterceptor: 负责权限校验的过滤器。
我们需要实现的接口:
-
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
-
AuthenticationManager接口:定义了认证Authentication的方法
-
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
-
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中
7.实现步骤
7.1基础登录校验
-
添加依赖
<!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
-
添加redis相关配置
@Configuration public class RedisConfig { @Resource private RedisConnectionFactory factory; @Bean public RedisTemplate redisTemplate(){ RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); //连接redis,设置连接工厂 redisTemplate.setConnectionFactory(factory); // 处理键值对数据序列化,不处理序列化会数据会乱码 // 针对key 和值都做序列化处理 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); // 特殊格式数据处理 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); om.setTimeZone(TimeZone.getDefault()); om.configure(MapperFeature.USE_ANNOTATIONS, false); om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); om.setSerializationInclusion(JsonInclude.Include.NON_NULL); return redisTemplate; }
-
响应类
/** * 接口通用的返回对象。 * * @author zyy */ public final class R<T> { private int code; private String msg; private T data; public R() { } public R(int code) { this.code = code; this.msg = ""; this.data = null; } public R(int code, String msg) { this.code = code; this.msg = msg; this.data = null; } public R(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public static R Success(Object data) { return new R(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMessage(), data); } public static R Success(String message, Object data) { return new R(ResultCodeEnum.SUCCESS.getCode(), message, data); } public static R Success() { return Success(""); } public static R Failed(String msg) { return new R(ResultCodeEnum.SYSTEM_EXCEPTION.getCode(), msg); } public static R Failed() { return Failed("Failed"); } public static R Failed(int code, String msg) { return new R(code, msg); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public boolean succeeded() { return getCode() == ResultCodeEnum.SUCCESS.getCode(); } public boolean failed() { return getCode() != ResultCodeEnum.SUCCESS.getCode(); } }
-
jwtTokenUtil工具类
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtTokenUtil { // 创建一个秘钥 public static final String TOKEN_KEY = "ZhengYY"; // 设置token的有效期 15min public final static long KEEP_TIME = 15 * 60 * 60 * 1000; /** * 生成token * * @param account 用户名 * @param accName * @return token */ public static String buildJwt(String account, String accName) { Date date = new Date(System.currentTimeMillis() + KEEP_TIME); Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY); // 设置头部信息 Map header = new HashMap<>(2); header.put("typ", "JWT"); header.put("alg", "HS256"); return JWT.create() .withHeader(header) .withClaim("account", account)//设置自定义内容 .withClaim("accName", accName) .withExpiresAt(date)//设置有效期 .sign(algorithm); } /** * 校验token是否正确 * * @param token * @return */ public static boolean verify(String token) { try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY); JWTVerifier verifier = JWT.require(algorithm) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getAccName(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("accName").asString(); } catch (JWTDecodeException e) { return null; } } /** * 获取登陆用户账号 * * @param token * @return */ public static String getAccount(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("account").asString(); } catch (JWTDecodeException e) { return null; } } }
-
使用SpringSecurity自带的密码校验格式校验用户
我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
5.1:准备数据库
/* Navicat Premium Data Transfer Source Server : localhost_3306 Source Server Type : MySQL Source Server Version : 80034 Source Host : localhost:3306 Source Schema : smart_from Target Server Type : MySQL Target Server Version : 80034 File Encoding : 65001 Date: 03/01/2024 11:43:33 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for account_info -- ---------------------------- DROP TABLE IF EXISTS `account_info`; CREATE TABLE `account_info` ( `account` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号,主键', `acc_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户姓名', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', `acc_phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号11位,唯一', `is_enable` tinyint(1) NOT NULL COMMENT '是否启用(1:启用,0:未启用)', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`account`) USING BTREE, UNIQUE INDEX `uk_phone`(`acc_phone` ASC) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
5.2:准备实体类
@Data @EqualsAndHashCode(callSuper = false) public class AccountInfo implements Serializable { private static final long serialVersionUID = 1L; /** * 用户账号,主键 */ private String account; /** * 用户姓名 */ private String accName; /** * 鐢ㄦ埛瀵嗙爜锛岄粯璁ゆ墜鏈哄彿鍚?浣? */ private String password; /** * 手机号11位,唯一 */ private String accPhone; /** * 是否启用(1:启用,0:未启用) */ private Boolean isEnable; /** * 创建时间 */ private Date createTime; /** * 更新时间 */ private Date updateTime;
5.3:因为UserDetailsService方法的返回值是UserDetails类型,所以我们需要定义一个类实现UserDetails接口,把用户信息封装到里面
@Data @NoArgsConstructor @AllArgsConstructor public class LoginAccountInfo implements UserDetails,Serializable{ private AccountInfo accountInfo; @Override public String getPassword() { return accountInfo.getPassword(); } @Override public String getUsername() { return accountInfo.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; }
5.4:创建一个类实现UserDetailsService接口,重写其中的方法。增加用户名从数据库中查询用户信息
@Service public class AccountInfoServiceImpl extends ServiceImpl<AccountInfoMapper, AccountInfo> implements AccountInfoService, UserDetailsService { @Autowired private AccountInfoMapper accountInfoMapper; /** * 重写Userdetails中的loadUserByUsername方法去查询数据库中的 * @param account * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { //查询用户信息 LambdaQueryWrapper<AccountInfo> lqw = new LambdaQueryWrapper<>(); lqw.eq(AccountInfo::getAccount,account); AccountInfo accountInfo = accountInfoMapper.selectOne(lqw); System.out.println(accountInfo); //没有查询到用户就抛出异常 if (ObjectUtils.isEmpty(accountInfo)){ throw new RuntimeException("账号获取密码错误"); } //TODO 查询对应的权限信息 //把数据封装成UserDetails return new LoginAccountInfo(accountInfo); } }
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop} 例如:
这样登录就可以使用YZ0002这个账号,123456作为密码登录
7.2使用Md5加密存储和校验
-
配置Md5Utils工具类,这个配置类要去实现SpringScurity提供的接口,并且重写它的转换密码和比较密码的两个方法
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.crypto.password.PasswordEncoder; import java.security.MessageDigest; /** * @ProjectName: Md5Utils.java * @Package: net.zlr.fengine.utils * @ClassName Md5Utils * @Author pengguo * @Description MD5加密 * @Date 11:36 2020/1/2 * @version v1.0.0 */ public class Md5Utils implements PasswordEncoder { private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); private static byte[] md5(String s) { MessageDigest algorithm; try { algorithm = MessageDigest.getInstance("MD5"); algorithm.reset(); algorithm.update(s.getBytes("UTF-8")); byte[] messageDigest = algorithm.digest(); return messageDigest; } catch (Exception e) { log.error("MD5 Error...", e); } return null; } private static final String toHex(byte hash[]) { if (hash == null) { return null; } StringBuffer buf = new StringBuffer(hash.length * 2); int i; for (i = 0; i < hash.length; i++) { if ((hash[i] & 0xff) < 0x10) { buf.append("0"); } buf.append(Long.toString(hash[i] & 0xff, 16)); } return buf.toString(); } public static String hash(String s) { try { return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8"); } catch (Exception e) { log.error("not supported charset...{}", e); return s; } } public String encode(CharSequence rawPassword) { //将rawPassword转换成MD5加密后的字符串 return hash(rawPassword.toString()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { //将rawPasswords是需要加密的密码和encodePassword是存入数据库的密码 String input = hash(rawPassword.toString()); return input.equals(encodedPassword.toString()); } }
-
定义一个SpringSecurity配置类,这个配置类继承WebSecurityConfigurerAdpter
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AccountInfoServiceImpl accountInfoService; @Autowired private JwtLoginTokenFilter jwtLoginTokenFilter; //创建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new Md5Utils(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过session获取SecurityContent .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //对于登录接口,允许匿名访问 .antMatchers("/accountInfo/login").anonymous()//permitAll()方法,无论有没有登录都可以访问 //除上面外的所有请求全部需要鉴证认证 .anyRequest().authenticated(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置认证方式,使用自定义的密码编码器 auth.userDetailsService(accountInfoService); } }
configure(AuthenticationManagerBuilder auth)
方法的作用是通过设置UserDetailsService
来指定如何获取用户的认证信息,以便在用户登录时进行认证。这样,Spring Security就能够根据你提供的认证方式,对用户进行身份验证和授权操作。 -
编写登录接口
@Autowired private AccountInfoService accountInfoService; //登录 @ApiOperation("用户登录") @PostMapping("/login") public R<Map<String,String>> accountLogin(@RequestBody AccountInfo accountInfo){ System.out.println(accountInfo); //登录 return accountInfoService.login(accountInfo); }
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
@Resource private RedisTemplate redisTemplate; @Autowired private AccountInfoMapper accountInfoMapper; @Autowired private AuthenticationManager authenticationManager; /** * 使用Security的登录 * @param accountInfo * @return */ public R<Map<String, String>> login(AccountInfo accountInfo) { //AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo.getAccount(),accountInfo.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //如果认证没通过给出对应的提升 if(ObjectUtils.isEmpty(authenticate)){ throw new RuntimeException("登录失败"); } //如果认证通过,使用userid生成一个jwt LoginAccountInfo loginAccountInfo = (LoginAccountInfo) authenticate.getPrincipal(); AccountInfo accountInfo1 = loginAccountInfo.getAccountInfo(); String account = accountInfo1.getAccount(); String accName = accountInfo1.getAccName(); String token = JwtTokenUtil.buildJwt(account, accName); Map<String,String> map = new HashMap<>(); map.put("token",token); //把完整的用户信息存入redis userid作为key redisTemplate.opsForValue().set(account,loginAccountInfo); return R.Success(map); }
这样配置之后就可以用一个使用md5加密后的账号进行测试
7.3认证过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
@Component
public class JwtLoginTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取Token
String token = request.getHeader("token");
if(token==null){
//放行
filterChain.doFilter(request,response);
return;
}
//解析Token
String account = JwtTokenUtil.getAccount(token);
//从redis中获取用户信息
String redisKey=account;
LoginAccountInfo accountInfo = (LoginAccountInfo) redisTemplate.opsForValue().get(redisKey);
System.out.println(accountInfo);
if (ObjectUtils.isEmpty(accountInfo)){
//用户未登录
R result = R.Failed("用户未登录");
String str = JSONObject.toJSONString(result);
response.setCharacterEncoding("utf-8");
response.getWriter().write(str);
return;
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,null,accountInfo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AccountInfoServiceImpl accountInfoService;
@Autowired
private JwtLoginTokenFilter jwtLoginTokenFilter;
//创建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new Md5Utils();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http //关闭csrf
.csrf().disable()
//不通过session获取SecurityContent
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口,允许匿名访问
.antMatchers("/accountInfo/login").anonymous()//permitAll()方法,无论有没有登录都可以访问
//除上面外的所有请求全部需要鉴证认证
.anyRequest().authenticated();
//添加Token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtLoginTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置认证方式,使用自定义的密码编码器
auth.userDetailsService(accountInfoService);
}
}
7.4退出登录
退出登录逻辑很简单:我们只需要定义一个退出登录的接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
/**
* 退出登录
* @return
*/
@Override
public R logout() {
//获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginAccountInfo accountInfo = (LoginAccountInfo) authentication.getPrincipal();
String account = accountInfo.getAccountInfo().getAccount();
//删除redis中的值
redisTemplate.delete(account);
return R.Success("退出成功");
}
7.5权限设置和校验
7.5.1限制接口访问权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
@EnableGlobalMethodSecurity(prePostEnabled = true)
这个加到SpringSecurity的配置类上面。
找到需要进行权限校验的接口,在方法上面加上@PreAuthoriza注解。
编写一个查询所有用户的接口
/**
* 查询所有用户的信息
* @return
*/
@PostMapping("/page")
@PreAuthorize("hasAnyAuthority('test')")
public R queryAccount(){
return accountInfoService.queryAccount();
}
/**
* 查询所有用户信息
**/
@Override
public R queryAccount() {
List<AccountInfo> accountInfos = accountInfoMapper.selectList(null);
return R.Success(accountInfos);
}
@PreAuthorize("hasAuthority('test')")
是Spring Security提供的注解之一,它的作用是在方法调用之前进行权限验证。
7.5.2封装权限信息
在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。
我们之前定义了UserDetails的实现类LoginAccountInfo,想要让其能封装权限信息就要对其进行修改。
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginAccountInfo implements UserDetails,Serializable{
private AccountInfo accountInfo;
private List<String> permissions;
public LoginAccountInfo(AccountInfo accountInfo, List<String> permissions) {
this.accountInfo = accountInfo;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
/**
* 获取权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permission中的string类型的权限信息封装成SimpleGrantedAuthority对象
// List<SimpleGrantedAuthority> newList = new ArrayList<>();
// for (String permission : permissions) {
// SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
// newList.add(authority);
// }
//使用函数编程简化
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return accountInfo.getPassword();
}
@Override
public String getUsername() {
return accountInfo.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;
}
}
LoginAccountInfo修改完后我们就可以在AccountInfoServiceImpl中去把权限信息封装到LoginAccountInfo中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。
/**
* 重写Userdetails中的loadUserByUsername方法去查询数据库中的
* @param account
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<AccountInfo> lqw = new LambdaQueryWrapper<>();
lqw.eq(AccountInfo::getAccount,account);
AccountInfo accountInfo = accountInfoMapper.selectOne(lqw);
System.out.println(accountInfo);
//没有查询到用户就抛出异常
if (ObjectUtils.isEmpty(accountInfo)){
throw new RuntimeException("账号获取密码错误");
}
//TODO 查询对应的权限信息
List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
//把数据封装成UserDetails
return new LoginAccountInfo(accountInfo,list);
}
校验的时候从redis中获取权限
@Component
public class JwtLoginTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取Token
String token = request.getHeader("token");
if(token==null){
//放行
filterChain.doFilter(request,response);
return;
}
//解析Token
String account = JwtTokenUtil.getAccount(token);
//从redis中获取用户信息
String redisKey=account;
LoginAccountInfo accountInfo = (LoginAccountInfo) redisTemplate.opsForValue().get(redisKey);
System.out.println(accountInfo);
if (ObjectUtils.isEmpty(accountInfo)){
//用户未登录
R result = R.Failed("用户未登录");
String str = JSONObject.toJSONString(result);
response.setCharacterEncoding("utf-8");
response.getWriter().write(str);
// throw new RuntimeException("用户未登录!");
return;
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,null,accountInfo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
测试权限校验,更改接口所需要的权限
@PreAuthorize("hasAnyAuthority('test1')")
出现不允许访问就测试成功
出现Bad credentials 则表示用户名或密码错误