Spring boot微服务体系下使用 Spring Security结合JWT技术实现权限认证方案

 

    项目采用spring boot 为基础微服务框架 spring security为安全控制框架 同时基础前后端分离的设计使用JWT来支持Token相关需求,但是了spring security并不支持这种无状态凭证,但是项目又需要用到这种无状态的身份认证及权限鉴定,所有有了如下方案。

方案大概流程如下图:

311b85508004a3df2061fe54a29d4379958.jpg

鉴权服务只负责发放和解析Token,任何一个服务的相关接口被访问前(包括内部服务访问和外部访问)都会先将Token传递给鉴权服务进行解析,当前服务拿到解析后的数据后注入到当前安全上下文后续的流程就交给spring security框架来处理了,具体的实现见下文。

为了实现上述方案,我们需要指定spring security拦截器的顺序通过查看文档得到下图

4d2935f783e45713ea1a9034b0def2e3189.jpg

其中SecurityContextPersistenceFilter很关键,这个拦截器负责从SecurityContextRepository中加载SecurityContext和保存SecurityContext,所以我们在这个地方实现SecurityContext注入是最好的,但是分析发现默认SecurityContextRepository的实现只有HttpSessionSecurityContextRepository和NullSecurityContextRepository,显然既然要无状态HttpSessionSecurityContextRepository不可以,NullSecurityContextRepository就是啥也不做所以也没用,那只能自己实现一个RemoteSecurityContextRepository

public class RemoteSecurityContextRepository implements SecurityContextRepository {
    private boolean isContext = false;
    

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        String token = Optional.ofNullable(requestResponseHolder.getRequest().getHeader(TokenConst.HEADER_STRING)).orElse("");
        JwtAuthenticationToken authentication = null;
        //TODO 这里实现从远程接口回去Token解析内容
        SecurityContextImpl context = new SecurityContextImpl();
        context.setAuthentication(authentication);
        isContext = true;
        return context;
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        //不用保存
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        return isContext;
    }
}

JwtAuthenticationToken 只要继承AbstractAuthenticationToken就好了可以自己随意添加属性。接下来将我们自己实现的RemoteSecurityContextRepository配置到框架内:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationProvider JwtAuthenticationProvider;
    @Autowired
    private SecurityContextRepository jwtSecurityContextRepository;


    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable().sessionManagement().disable().formLogin().disable().cors().disable();
        httpSecurity.securityContext().securityContextRepository(jwtSecurityContextRepository);
        httpSecurity.authorizeRequests()
                .antMatchers("/v2/api-docs").hasAuthority("ROLE_DEV")
                .antMatchers("/monitor").hasAuthority("ROLE_MONITOR")
                .antMatchers("/api/private/**", "/api/protected/**").authenticated()
                .and().authenticationProvider(JwtAuthenticationProvider)
                .exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint());

    }


}

jwtSecurityContextRepository就是RemoteSecurityContextRepository的实例bean,这样当需要被验证接口请求到达时就会进行Token解析然后讲返回的信息添加到安全上下文,后续spring security会进行接管,但是这样就够了吗?显然不行。

如果只进行到毫无意外spring security会一律返回http 403 ,为什么了?在JwtAuthenticationToken中有一个很关键的属性叫Authenticated,当Authenticated的值是true时表示当前安全上下文是经过验证的后续只需要进行权限判断(AccessDecisionManager),但是这个时候我们还不清楚请求所携带的身份信息是否合法,是否过期,那怎么办了?这就需要配置AuthenticationProvider实例并且将Authenticated设置为false。

当然框架默认提供的AuthenticationProvider实例所支持的功能也是够的但是不能满足细化的控制结果要求,所以我们自己实现一个

public class JwtAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (ObjectUtil.isEmptyObject(authentication)) {
            throw new AuthenticationCredentialsNotFoundException("");
        }
        JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;

        //非法证书 返回403
        if (!jwtToken.isCredentialsAuthorized()) {
            throw new BadCredentialsException("");
        }
        //凭证过期 从新登陆
        if (jwtToken.isCredentialsExpired()) {
            throw new CredentialsExpiredException("");
        }
        //账户异常 从新登陆
        if (!jwtToken.isAccountNormal()) {
            throw new CredentialsExpiredException("");
        }
        jwtToken.setAuthenticated(true);
        return jwtToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.isAssignableFrom(JwtAuthenticationToken.class);
    }
}

这样我们就可以针对不同的情况进行不同的返回,但是到这你会发现不管抛出什么异常,spring security都只给http 403 ,这显然不行,那怎么办了,通过阅读文档发现有一个属性在干这个事AuthenticationEntryPoint,通过该属性设置的bean会接管所有验证阶段抛出的异常,我们就可以自定义实现一个类并设置来达到针对不同的异常进行不同的返回。

public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (authException instanceof CredentialsExpiredException) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        } else if (authException instanceof BadCredentialsException) {
            response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
        } else if (authException instanceof AuthenticationCredentialsNotFoundException) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        } else if (authException instanceof AuthenticationCredentialsNotFoundException) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        } else {
            log.error(authException.getLocalizedMessage(), authException);
            response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
        }

    }
}

做到这里就完全实现了jwt和spring security的结合,从而实现权限控制

 

 

转载于:https://my.oschina.net/wtfshy/blog/3059849

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值