基于路径简单整合SpringSecurity+Gateway

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

GitHub - Createsequence/mybatis-plus-join: 基于myabtis-plus的连表查询扩展,支持字段别名、预设条件、group by ... having、数据库函数等扩展功能

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;
    }


}

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值