SpringSecurity是一个功能强大且高度可定制的认证和访问控制框架,提供了一系列高可配置的安全功能,但是我在整合Gateway时却发现力不从心,因为Gateway的内核是基于WebFlux的API网关,所以无法简单的将Gateway跟Springsecurity整合在一个模块之中,遂分享解决办法。
实际使用下来SpringSecurity基本被架空,仅做学习参考,初学者,如有错误请一定要指出
SpringSecurity大致流程
我们可以将SpringSecurity通过核心功能简单分为两个部分:
(1)认证:通过配置过滤器链和自己手写过滤器完成过滤拦截以及认证
(2)授权:通过实现UserDetails接口并调用getAuthorities方法将权限注入,在方法上使用注解@PreAuthorize("hasAuthority('permission')")来检查是否拥有访问的权限
但是正如我所遇到的问题,不能将SpringSecurity跟Gateway放一个模块,导致SpringSecurity提供的方法都不能很好的使用,所以我采用了将SpringSecurity的过滤拦截鉴权交由Gateway处理,SpringSecurity仅做登录授权的方法,下面是我的实现过程
SpringSecurity登录授权功能的代码实现
这一段代码完成了以下几个功能:
(1)将用户的账号密码包装成UsernamePasswordAuthenticationToken类型,并传给AuthenticationManager
(2)AuthenticationManager调用AbstractUserDetailsAuthenticationProvider中的DaoAuthenticationProvider方法
(3)DaoAuthenticationProvider调用UserDetailsService中的loadUserByUsername方法
@Service
public class AccountInfoServiceImpl extends ServiceImpl<AccountInfoMapper, AccountInfo> implements AccountInfoService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AccountRoleMapper accountRoleMapper;
@Autowired
private RedisCache redisCache;
@Override
public R login(UserLoginDTO userLoginDTO) {
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(userLoginDTO.getAccountName(),userLoginDTO.getPassword());
//传入账号密码后,会首先根据账号查询到密码,然后将查询到的密码跟传入的密码做比较
Authentication authentication = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if(ObjectUtils.isEmpty(authentication)){
return R.Failed("登录失败");
}
//如果认证通过了,使用id生成换一个jwt jwt返回给前端
LoginUser loginUser=(LoginUser)authentication.getPrincipal();
String id=loginUser.getAccountInfo().getId().toString();
//把完整的用户信息存入redis,id作为key
redisCache.setCacheObject(id,loginUser,30,TimeUnit.MINUTES);
//将过期判断交给redis,若redis数据过期则会在过滤器链中抛出token异常
return R.Success("登录成功",JwtUtil.createJWT(id));
}
}
(4)在loadUserByUsername中根据传入的账号名来查找相关信息
(5)如果账号存在则根据账号id来查找对应权限
(6)将用户跟权限列表封装进LoginUser
(7)DaoAuthenticationProvider调用PasswordEncoder对比UserDetails中的密码跟封装成UsernamePasswordAuthenticationToken的用户传入的密码,如果相同则登陆成功
(8)登录成功后以返回的LoginUser的id为key将用户信息存入redis中,并返回Jwt加密后的id
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Autowired
private AccountRoleMapper accountRoleMapper;
public UserDetails loadUserByUsername(String accountName) throws UsernameNotFoundException{
LambdaQueryWrapper<AccountInfo> lambdaQueryWrapper=new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(!ObjectUtils.isEmpty(accountName),
AccountInfo::getAccountName,accountName)
.eq(AccountInfo::getDeleteFlag,"未删除")
.eq(AccountInfo::getIsEnable,"启用");
AccountInfo accountInfo=this.accountInfoMapper.selectOne(lambdaQueryWrapper);
if(ObjectUtils.isEmpty(accountInfo)){
throw new UsernameNotFoundException("该账号不存在");
}
//查询对应的权限信息
MPJLambdaWrapper<AccountRole> mpjlambdaWrapper=new MPJLambdaWrapper<>();
mpjlambdaWrapper.select(PermissionList::getPermissionName)
.leftJoin(Role.class,Role::getRoleId,AccountRole::getRoleId)
.leftJoin(RolePermission.class,RolePermission::getRoleId,AccountRole::getRoleId)
.leftJoin(PermissionList.class,PermissionList::getId,RolePermission::getPermissionId)
.eq(AccountRole::getAccountId,accountInfo.getId());
//权限列表
List<String> permissionNameList = accountRoleMapper.selectJoinList(String.class, mpjlambdaWrapper);
return new LoginUser(accountInfo,permissionNameList);
}
}
LoginUser:因为鉴权功能交给了Gateway,所以实际上只需要用到accountInfo跟permissionList
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
//写入自己的用户类来存储信息
private AccountInfo accountInfo;
//用户权限列表
private List<String> permissionList;
//权限列表
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
//获取权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(ObjectUtils.isEmpty(authorities)) {
return permissionList.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
return authorities;
}
public LoginUser(AccountInfo accountInfo,List<String> permissionList){
this.permissionList=permissionList;
this.accountInfo=accountInfo;
}
@Override
public String getPassword() {
return accountInfo.getPassword();
}
@Override
public String getUsername() {
return accountInfo.getAccountName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
到这里,所需要的SpringSecurity的功能都做完了
GateWay大致流程
工具类
(1)isWhiteList:读取yml文件"whiteList.yml",将请求路径与白名单路径作比较,若匹配则放行
(2)hasPermission:读取yml文件"permissionMap.yml",将用户权限以及请求路径传入,若用户权限与请求路径所需权限匹配则放行
(3)out:返回体
public class MyUtil {
//路径是否属于白名单
public static boolean isWhiteList(String path){
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(new ClassPathResource("whiteList.yml"));
Properties properties = yamlPropertiesFactoryBean.getObject();
List<String> whiteList = new ArrayList<>();
if (properties != null) {
for (int i = 0; properties.containsKey("whiteList[" + i + "]"); i++) {
whiteList.add((String) properties.get("whiteList[" + i + "]"));
}
}
//匹配
return whiteList.stream().anyMatch(s -> s.equals(path));
}
//权限列表是否拥有权限访问路径的权限
public static boolean hasPermission(List<String> permissionList,String path){
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(new ClassPathResource("permissionMap.yml"));
Properties properties = yamlPropertiesFactoryBean.getObject();
Map<String, String> permissionMap = new HashMap<>();
if (properties != null) {
for (int i = 0; properties.containsKey("permissionMap[" + i + "].url"); i++) {
permissionMap.put
((String)properties.get("permissionMap[" + i + "].url"),
(String)properties.get("permissionMap[" + i + "].permission") );
}
}
//匹配
return permissionList.stream().anyMatch(s -> s.equals(permissionMap.get(path)));
}
//返回体
public static Mono<Void> out(ServerHttpResponse response, String data) {
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 28004);
message.addProperty("data", data);
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
permissionMap.yml 文件格式如下
whiteList.yml 文件格式如下
GateWay过滤拦截鉴权功能的代码实现
大致流程跟流程图基本一致
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private RedisCache redisCache;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
//获取token
String token = Optional.ofNullable(request.getHeaders().get("token"))
.filter(tokenList -> !tokenList.isEmpty())
.map(tokens -> tokens.get(0))
.orElse(null);
//如果token为空
if (token == null) {
//如果路径属于白名单则放行
if (MyUtil.isWhiteList(path)) {
return chain.filter(exchange);
}
//如果token为空且路径不在白名单则返回
else {
return MyUtil.out(exchange.getResponse(), "token为空");
}
}
//如果token不为空
else {
try {
//将token转成id并获取对象
LoginUser loginUser = redisCache.getCacheObject(JwtUtil.parseJWT(token).getSubject());
//将loginUser放入exchange中
exchange.getAttributes().put("loginUser", loginUser);
//如果不能成功获取对象则说明该token非法
if (ObjectUtils.isEmpty(loginUser)) {
return MyUtil.out(exchange.getResponse(), "token非法");
}
} catch (Exception e) {
return MyUtil.out(exchange.getResponse(), "token非法");
}
}
return chain.filter(exchange);
}
//表示执行顺序,数字越小优先级越高
@Override
public int getOrder() {
return 0;
}
}
public class MyPermissionFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取路径
String path = Optional.ofNullable(exchange.getRequest().getURI().getPath())
.orElse(" Illegal path");
//如果路径属于白名单则放行
if(MyUtil.isWhiteList(path)){
return chain.filter(exchange);
}
//获取权限列表
List<String> permissionList= Optional.ofNullable((LoginUser) exchange.getAttributes().get("loginUser"))
.map(LoginUser::getPermissionList)
.orElse(null);
//如果拥有权限则放行
if(MyUtil.hasPermission(permissionList,path)){
return chain.filter(exchange);
}
//如果不存在则拦截
return MyUtil.out(exchange.getResponse(),"权限不足或路径不存在");
}
@Override
public int getOrder() {
return 1;
}
}