基于Spring Security OAuth2.0、JwtToken的微服务鉴权Demo

一、前记
记得很久之前面试美的,当时吹水微服务,被人问到一个问题:微服务鉴权如何处理的?嗯,很简单一个问题,但是懵逼了(水平不行啊),自然就挂了,后面就想以后吹水微服务一定要做好鉴权这一块的内容。

二、准备知识
Spring Security、OAuth2.0、Jwt;网上有很多内容,这里不赘述。
OAuth2.0主要学几种模式,如密码模式、客户端模式等等,这里我们使用密码模式。

三、参考文章
本文参考了这几个大神的文章,1)https://www.jianshu.com/p/24764aba1012?utm_source=oschina-app;
2)https://www.jianshu.com/p/59dea41787c9
第一个文章是Spring Cloud相关内容,不是特别适合入门,所以我去除了SC的内容;第一篇改为内置InMemory数据库;第一篇文章请求Jwt token使用了SC的FeignClient,返回的Jwt对象还自己写pojo定义了,实际上Spring Security OAuth已经有请求相关对象(ResourceOwnerPasswordAccessTokenProvider,谢谢第二篇),所以我进行了替换;第一篇文章认证服务器、资源服务器分开部署,有两个bean同名容易困扰,本篇认证服务器、资源服务器是统一部署的,没有混杂的东西,新手容易入手;第一篇还有一个Jwt公钥Public.cert直接保存在服务器读取的,不是很安全啊,我没有用这种方式。

可以说两篇融(chao)合(xi)了这两篇以后,加上自己的思考,就有了我这第三篇。

四、微服务请求流程
盗第一篇的图:
基于Spring Security OAuth2.0、JwtToken的微服务鉴权Demo_第1张图片
我是这样设计的,开放三个rest api对外提供访问:login、user、admin;其中user需要user权限的用户可以访问、admin需要admin用户权限、login无限制访问,用户名密码认证成功后返回access token,后续访问user、admin携带该token,权限无误后可以访问。

五、程序设计
1)WebSecurityConfig
功能:提供内置用户名密码数据库、提供AuthenticationManager给OAuth使用、配置需要认证的路径

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter

a.建立test、admin、dba账户,对应不同权限;passwordEncoder是BCryptPasswordEncoder

@Autowired
private PasswordEncoder passwordEncoder;

@Bean
PasswordEncoder passwordEncoder(){
  return new BCryptPasswordEncoder();
}


@Bean
public UserDetailsService userDetailsService(){
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("test").password(passwordEncoder.encode("test")).roles("USER").build());
    manager.createUser(User.withUsername("admin").password(passwordEncoder.encode("admin")).roles("USER","ADMIN").build());
    manager.createUser(User.withUsername("dba").password(passwordEncoder.encode("dba")).roles("USER","DBA").build());
    return manager;
}

b.配置认证路径(Spring Security)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http

            .csrf().disable()  // 使用jwt,可以允许跨域
            .exceptionHandling() // 错误处理
            .authenticationEntryPoint(new UnauthorizedEntryPoint()) // 内部类
            .and()
            .authorizeRequests() 
            //.antMatchers("/oauth/token").permitAll()
            .antMatchers("/**").authenticated() // 所有请求都要认证
            .and().httpBasic(); // http_basic方式进行认证

}

自己定义了一个内部类用于错误处理,假如是ajax请求,错误返回未认证错误信息,假如非ajax,则跳转到特定页面(当然我没有这个页面,这里懒得改了)

  class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        if (isAjaxRequest(request)) {
            // ajax请求,返回错误代码
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
        } else {
            // 非ajax请求,跳转到login界面
            response.sendRedirect("/login");
        }

    }

    public boolean isAjaxRequest(HttpServletRequest request) {
        String ajaxFlag = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(ajaxFlag);
    }
}

c.定义一个bean名称为MyAuthManager,使得后面OAuth服务可以使用我们这里定义的认证管理器AuthenticationManager

 @Bean(name = "MyAuthManager")
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

2)Jwt Token配置
功能:主要是提供token转换器、token存储(本文为内存存储,真正一般使用redis)

@Configuration
public class JwtConfig {

    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() throws IOException{
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("cnsesan-jwt.jks"),
                "cnsesan123".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("cnsesan-jwt"));
        return converter;
    }

    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter converter){
        return new JwtTokenStore(converter);
    }
}

a.TokenConverter是读取cnsesan-jwt.jks证书文件,使用cnsesan123这个密钥对证书进行解密(关于如何生成证书,参考第一篇,用keytool工具,百度上也有一堆)
为了保险,POM文件加入这个,证书文件打包时不要重新编码

    
    
        org.apache.maven.plugins
        maven-resources-plugin
        
            cert
            jks
        
    

b.使用这个converter生成TokenStore,也就是后续token的存储都在这个store,使用你这个converter去加解密

3)OAuth授权服务器
@Configuration
@EnableAuthorizationServer // 开启授权服务功能
public class OAuthAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter

首先,授权服务器使用了2)中Jwt Token的相关内容,客户端密码secret使用BCryptPasswordEncoder(这个必须的,否则请求token报错,文章一没有使用加密secret,不知道是不是spring security版本问题

@Autowired
TokenStore tokenStore;

@Autowired
JwtAccessTokenConverter jwtTokenEnhancer;

@Autowired
PasswordEncoder passwordEncoder;


@Resource(name = "MyAuthManager")
private AuthenticationManager authenticationManager; // 注入前面1)WebSecurityConfig定义的认证管理器

// 配置客户端基本信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory().withClient("user-service")// 创建一个客户端 名字是user-service
            .secret(passwordEncoder.encode("123456")) // 必须用加密存储的方式,否则401错误
            .scopes("service")
            .authorizedGrantTypes("refresh_token", "password")  // 密码模式
            .accessTokenValiditySeconds(3600); // token有效期3600s
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
   // 定义相关tokenStore、token加解密转换器、认证管理器
    endpoints.tokenStore(tokenStore).tokenEnhancer(jwtTokenEnhancer)
            .authenticationManager(authenticationManager);
}


@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer
            .tokenKeyAccess("permitAll()")    // 允许/oauth/token被调用,默认deny,不过经测试这一个可以不加
            .checkTokenAccess("permitAll()")    // 允许所有检查token,默认deny,这个必须加,否则check_token不能访问显示401未授权错误
            .passwordEncoder(passwordEncoder); // 定义认证服务器的client secrets密码加密方式

}

使用POSTMAN检查token:
127.0.0.1:9000/oauth/check_token?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTcxMzYzMzEsInVzZXJfbmFtZSI6InRlc3QiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMDY4YWE4NGMtYTlkOS00ZjlhLWJmMzYtODNiNWU2NTQ4M2EyIiwiY2xpZW50X2lkIjoidXNlci1zZXJ2aWNlIiwic2NvcGUiOlsic2VydmljZSJdfQ.T_lbaIB9WVSredAw1bldSX7CcNhv0Y_U_jzV_IPRYfPxbuJQvCZT2qlaaVm6Y7GE8L8xXTgVm22_Gh7qO7R0Pdrw8JURYQGmmn0wmzR2AowTl2R6cy_5hGdhRt1Qk0JB1D9mu_Wpq1LJkyDD_6Hwf_F0xqPi41iRVJ2w00C-4rqm7EjriNZplmNBFIZKvLzxuo4Tj0wkkjZKmB0v5V3PJPmXrXdbWnjo1gXK_4H5Aosd0810X1d6UHyzzYPQi62bQD84kZfKpdxwM2szVtWLoDM5Cp_iy2y8QIKKPg_iM78PUg9UQQh1EQDfnO8Dg0guBLE-tgB2bVDBwRXNYB_Fzg

返回值为:
{
“exp”: 1557136331,
“user_name”: “test”,
“authorities”: [
“ROLE_USER”
],
“jti”: “068aa84c-a9d9-4f9a-bf36-83b5e65483a2”,
“client_id”: “user-service”,
“scope”: [
“service”
]
}

可见,对token实现了解密。

4)OAuth资源管理器
定义资源访问权限,哪些资源是受限访问

@Configuration
@EnableResourceServer
public class OAuthResourceServerConfig extends ResourceServerConfigurerAdapter {
	 @Autowired
	 TokenStore tokenStore ; // 使用JwtConfig的
	

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login","/register").permitAll() // login资源是开放的
                .antMatchers("/user").hasRole("USER")
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers("/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenStore(tokenStore);

    }
}

好了,到此为止,我们已经构建了授权服务器、资源服务器想换内容,下面可以写我们的业务逻辑代码了!!

5)用户登录获取token凭证的服务UserLoginAndGetTokenSvc
首先使用ResourceOwnerPasswordResourceDetails构建一个请求凭证,向oauth/token进行请求,假如请求成功,返回token,否则返回错误OAuth2AccessDeniedException。文章一中请求token,先要自己去检查用户名 密码,不匹配抛出错误,正确的再去换取token。其实发起oauth/token请求后就自动去验证用户名密码是否匹配,文章一方式太拖沓了。我猜文章一这种方式的原因是,他使用FeignClient进行token请求,假如用户名密码不匹配,FeignClient的报错信息很不明确,原始oauth服务器的信息被丢掉了.(/oauth/token在密码用户名不匹配时抛出401而不是500,因此FeignClient不捕获??)各位看官自己可以验证。

@Service
public class UserLoginAndGetTokenSvc {
    // 组装请求token凭证
    private ResourceOwnerPasswordResourceDetails packPasswordResourceDetails(String clientId, String clientSecret, String username, String password, String... scopes){
        ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
        //String cryptPsw = base64Encoder.encodeToString(password.getBytes());
        //设置请求认证授权的服务器的地址
        details.setAccessTokenUri("http://localhost:9000/oauth/token");
        //下面都是认证信息:所拥有的权限,认证的客户端,具体的用户
        details.setScope(Arrays.asList(scopes));
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setUsername(username);
        details.setPassword(password);
        return details;

    }
    public OAuth2AccessToken login(String username, String password) throws OAuth2AccessDeniedException{

        String clientId = "user-service";
        String clientSecret = "123456";
        String[] scopes = {"service"};
        ResourceOwnerPasswordAccessTokenProvider provider = new ResourceOwnerPasswordAccessTokenProvider();
        OAuth2AccessToken accessToken = null;
        try {
            //获取AccessToken
            // 1、(内部流程简介:根据上述信息,将构造一个前文一中的请求头为 "Basic Base64(username:password)" 的http请求
            //2、之后将向认证授权服务器的 oauth/token 端点发送请求,试图获取AccessToken
            ResourceOwnerPasswordResourceDetails details = packPasswordResourceDetails(clientId, clientSecret, username, password, scopes);
            accessToken = provider.obtainAccessToken(details, new DefaultAccessTokenRequest());
        }catch(OAuth2AccessDeniedException ex){
            throw new OAuth2AccessDeniedException("获取jwt token出错,原因为:" + ex.getCause().getMessage());
        }
        return accessToken;

    }

}

小知识,向oauth/token请求时,一般而言,scope、username、password、grant_type作为请求参数,request header插入:Authorization:"Basic " + Base64(clientId:clientSecret),即插入Basic 与clientId:clientSecret的Base64编码

到此,获得jwt token的服务 LoginAndGetTokenSvc 已经构造完毕,下文将介绍RestController,它使用上述服务来获得token返回。

6)Rest Controller
构造三个rest api,login负责获得token并返回,user只允许权限为ROLE_USER的用户访问,admin值允许权限为ROLE_ADMIN的用户访问。太简单了,就不解释了。

@RestController
public class Business {
    @Autowired
    UserLoginAndGetTokenSvc loginSvc;


    @RequestMapping("/login")
    public OAuth2AccessToken login(@RequestParam String username, @RequestParam String password) throws OAuth2AccessDeniedException {
        OAuth2AccessToken token = loginSvc.login(username,password);
        return token;
    }

    @RequestMapping("/user")
    public String onlyUserVisit(){
        return "You r USER";
    }

    @RequestMapping("/admin")
    public String onlyAdminVisit(){
        return "You r ADMIN";
    }
}

7)源码下载地址如下:
https://download.csdn.net/download/xiaxuepiaopiao/11163495
设置了积分,嘻嘻,各位看官行行好,毕竟手码不容易~~~

你可能感兴趣的:(微服务鉴权)