SpringCloud Alibaba整合Oauth2,博文有点长耐心看完不说精通,至少从零搭建没问题了
SpringCloudOauth2
Oauth2简介
OAuth 2.0是用于授权的行业标准协议。OAuth 2.0致力于简化客户端开发人员,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的授权流程。该规范及其扩展正在IETF OAuth工作组内开发。
OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。OAuth 的核心就是向第三方应用颁发令牌。然后,RFC 6749 接着写道:它定义了获得令牌的四种授权方式(authorization grant )。
授权流程图
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
简单一点的诉述就是发起一个认证请求,根据授权类型去认证服务器认证,如果成功就返回token,再使用token去访问资源服务器,token验证通过就返回被保护的资源。
包含的角色
OAuth定义了四个角色:
资源所有者:能够授予对受保护资源的访问权限的实体。当资源所有者是一个人时,它称为最终用户。
资源服务器:托管受保护资源的服务器,能够接受并使用访问令牌响应受保护的资源请求。
客户端:对我们的产品来说,QQ、微信登录是第三方登录系统。我们又需要第三方登录系统的资源(头像、昵称等)
授权服务器:请求授权成功后,服务器向客户端发布访问令牌认证资源所有者并获得授权
授权模式
-
授权码(authorization-code)
-
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
https://服务器IP:服务器PORT/oauth/authorize? response_type=code& # 固定写法 client_id=CLIENT_ID& # client_id 自己定义的 redirect_uri=CALLBACK_URL& #回调地址,授权成功跳转到地址并携带code=XXXX,之后再用XXXX去申请令牌 scope=read #非必须
-
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回
redirect_uri
参数指定的网址。跳转时,会传回一个授权码,就像下面这样。https://CALLBACK_URL?code=AUTHORIZATION_CODE
-
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
https://服务器IP:服务器PORT/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
上面 URL 中,
client_id
参数和client_secret
参数用来让 B 确认 A 的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是authorization_code
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。 -
第四步,B 网站收到请求以后,就会颁发令牌。
{ "access_token": "0e6d26f3-3ad3-407f-8203-3342f25807ca", "token_type": "bearer", "refresh_token": "9941dc19-a167-4173-9895-934f9519363d", "expires_in": 86399, "scope": "all", "username1": "demo", "license": "wujie" }
-
-
隐藏式(implicit)
-
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
-
第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用
-
https://服务器IP:服务器PORT/oauth/authorize? response_type=token& #response_type参数为token,表示要求直接返回令牌 client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
-
第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回
redirect_uri
参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。 -
https://服务器IP:服务器PORT/callback#token=ACCESS_TOKEN #token=ACCESS_TOKEN这里就是token
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
这种方式安全性很低,所以一般用于对安全性要求不高的场景,并且token有效期很短,一般也就是当前session,会话结束就失效。
-
-
密码式(password):
-
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
-
第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
-
https://服务器IP:服务器PORT/oauth/token? grant_type=password& #password指明使用密码模式 username=USERNAME& #用户名 password=PASSWORD& #密码 client_id=CLIENT_ID #定义的client
-
第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
-
{ "access_token": "0e6d26f3-3ad3-407f-8203-3342f25807ca", "token_type": "bearer", "refresh_token": "9941dc19-a167-4173-9895-934f9519363d", "expires_in": 86399, "scope": "all", "username1": "demo", "license": "wujie" }
-
这种方式毕竟是直接给出了用户的账号密码,最好是在自己的系统内部使用的场景。如果不是都必须是一种相互高度信任的场景
-
-
客户端凭证(client credentials)
-
用于没有前端的命令行应用,即在命令行下请求令牌。
-
第一步,A 应用在命令行向 B 发出请求。
https://服务器IP:服务器PORT/oauth/token? grant_type=client_credentials& #指定使用客户端凭证模式 client_id=CLIENT_ID& #定义的client client_secret=CLIENT_SECRET #定义的secret
-
第二步,B 网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
-
注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端**(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
刷新令牌
OAuth 2.0 允许用户自动更新令牌,当令牌的有效期到了以后,如果让用户重新走一次上面的流程,对于用户的体验来说相当的不友好。
{ "access_token": "0e6d26f3-3ad3-407f-8203-3342f25807ca", "token_type": "bearer", "refresh_token": "9941dc19-a167-4173-9895-934f9519363d", "expires_in": 86399, "scope": "all", "username1": "demo", "license": "wujie" }
在我们上面请求到的token中,都是同时颁发了access_token以及refresh_token。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://服务器IP:服务器PORT/oauth/token? grant_type=refresh_token& #指定是刷新令牌 client_id=CLIENT_ID& #定义的client client_secret=CLIENT_SECRET& #定义的secret refresh_token=REFRESH_TOKEN #使用获取令牌时同时返回的refresh_token
B 网站验证通过以后,就会颁发新的令牌。
Spring Security
简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求
特点
-
对身份验证和授权的全面且可扩展的支持
-
防止攻击,例如会话固定,点击劫持,跨站点请求伪造等
-
Servlet API集成
-
与Spring Web MVC的可选集成
Spring Cloud Security
简介
Spring Cloud Security提供了一组原语,用于以最少的麻烦构建安全的应用程序和服务。可以在外部(或中央)进行大量配置的声明式模型通常可以通过中央身份管理服务来实现大型的,相互协作的远程组件系统。在Cloud Foundry等服务平台中使用它也非常容易。在Spring Boot和Spring Security OAuth2的基础上,我们可以快速创建实现常见模式(如单点登录,令牌中继和令牌交换)的系统。
特点
-
在Zuul代理中将SSO令牌从前端中继到后端服务
-
资源服务器之间的中继令牌
-
使Feign客户端行为类似于OAuth2RestTemplate的拦截器(获取令牌等)
-
在Zuul代理中配置下游身份验证
说了这么多东西,其实没有什么用处,就是简单的普及一下安全相关的技术,而且根据最新的相关资料显示,Spring官方已经不支持Spring Security Oauth2了,他们将对这技术进行迁移,换句话说就是打算自己搞一个。
有兴趣的可以自己去了解阅读一下。
咱们步入正题吧。继续使用我们之前搭建好的SpringCloudAlibaba的框架,我们再新建一个项目。
创建授权服务
pom.xml的配置文件如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.wujie</groupId> <version>0.0.1-SNAPSHOT</version> <artifactId>hello-spring-cloud-alibaba-dependencies</artifactId> </parent> <groupId>com.wujie</groupId> <artifactId>hello-spring-cloud-alibaba-nacos-oauth-authorization</artifactId> <version>0.0.1-SNAPSHOT</version> <name>hello-spring-cloud-alibaba-nacos-oauth-authorization</name> <description>创建授权服务器</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--freemarker,页面渲染引擎--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>2.7.9</version> </dependency> <!-- mybatis启动器 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <!-- 通用Mapper启动器 --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency> <!-- 分页助手启动器 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.0.RELEASE</version> <configuration> <mainClass> com.wujie.hello.spring.cloud.alibaba.nacos.oauth.authorization.HelloSpringCloudAlibabaNacosOauthAuthorizationApplication </mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
我们这里直接引入spring-cloud-starter-oauth2,由于这个包已经包含了spring-cloud-starter-security,所以不需要在单独进行引入。我们还加了一个mysql的驱动包这是因为打算使用数据库来管理客户端,还加入了tk-mybatis的包,打算稍微正式一点从数据库里获取用户信息以及权限。还引入了redis的包用来存储token。
application.yml
spring: application: name: hello-spring-cloud-alibaba-nacos-oauth-authorization cloud: nacos: discovery: server-addr: localhost:8848 freemarker: suffix: .ftl #springboot2.2以后的版本默认的是ftlh,如果不配置的话会找不到ftl redis: host: 127.0.0.1 port: 6379 management: endpoints: web: exposure: include: "*" main: allow-bean-definition-overriding: true datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver username: 数据库账号 password: 数据库密码 hikari: maximum-pool-size: 30 minimum-idle: 10 auto-commit: true idle-timeout: 30000 pool-name: DatebookHikariCP max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 jdbc-url: jdbc:mysql://localhost:3306/oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true url: jdbc:mysql://localhost:3306/oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true server: port: 8766 logging.level.org.springframework.security: DEBUG
创建一个授权的配置文件
根据官方指示我们需要创建一个配置类去实现AuthorizationServerConfigurer
所以我们,创建一个类去继承它的实现类AuthorizationServerConfigurerAdapter
,具体代码如下:
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired UserDetailsServiceImpl userDetailsServiceImpl; /** * 配置tokenStore的存储方式是redis存储 * @return */ @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //配置允许表单访问 security.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()") .checkTokenAccess("permitAll()"); } /** * 配置客户端的管理是jdbc * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(new JdbcClientDetailsService(dataSource)); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer())); endpoints.authenticationManager(authenticationManager) .tokenEnhancer(tokenEnhancerChain) //配置tokenStore管理、配置客户端详情 .tokenStore(tokenStore()).userDetailsService(userDetailsServiceImpl) //配置授权模式 .tokenGranter(tokenGranter(endpoints)); //配置tokenServices的参数 DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); //配置accessToken过期时间 defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(2)); //配置refreshToken的过期时间 defaultTokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); //设置支持刷新token defaultTokenServices.setReuseRefreshToken(true); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(endpoints.getTokenStore()); defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService()); defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); endpoints.tokenServices(defaultTokenServices); } /** * 配置授权模式也可以添加自定义模式(不写也有默认的) * 具体可查看AuthorizationServerEndpointsConfigurer中的getDefaultTokenGranters方法 * 以后添加一个手机验证码的功能 * * @param endpoints * @return */ private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) { List<TokenGranter> list = new ArrayList<>(); //增加刷新token list.add(new RefreshTokenGranter(endpoints.getTokenServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory())); //授权码模式 list.add(new AuthorizationCodeTokenGranter(endpoints.getTokenServices(),endpoints.getAuthorizationCodeServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory())); //客户端凭证模式 list.add(new ClientCredentialsTokenGranter(endpoints.getTokenServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory())); //密码模式 list.add(new ResourceOwnerPasswordTokenGranter(authenticationManager,endpoints.getTokenServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory())); //隐藏式 list.add(new ImplicitTokenGranter(endpoints.getTokenServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory())); return new CompositeTokenGranter(list); } /** * 创建一个token增强方法,增加一些我们自己想要返回的附加信息 * @return */ @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { final Map<String, Object> additionalInfo = new HashMap<>(2); additionalInfo.put("license", "wujie"); UserDetailsDto user = (UserDetailsDto) authentication.getUserAuthentication().getPrincipal(); if (user != null) { additionalInfo.put("phone", user.getPhone()); additionalInfo.put("id", user.getId()); } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; }; } }
由于授权服务器本身也是资源服务器,所以也创建一个资源配置,代码如下
@Configuration // 启用资源服务 @EnableResourceServer // 启用方法级权限控制 @EnableGlobalMethodSecurity(prePostEnabled = true) @Log4j2 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { /** * 配置资源接口安全,http.authorizeRequests()针对的所有url,但是由于登录页面url包含在其中,这里配置会进行token校验,校验不通过返回错误json, * 而授权码模式获取code时需要重定向登录页面,重定向过程并不能携带token,所有不能用http.authorizeRequests(), * 而是用requestMatchers().antMatchers(""),这里配置的是需要资源接口拦截的url数组 * @param http * @return void */ @Override public void configure(HttpSecurity http) throws Exception { http //配置需要保护的资源接口 .requestMatchers().antMatchers("/user","/test/need_token","/logout","/remove","/update","/test/need_admin","/test/scope") .and().authorizeRequests().anyRequest().authenticated(); } }
然后在创建一个webSecurity的配置,代码如下
@Configuration @EnableWebSecurity() public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private IgnoreLogoutFilter ignoreLogoutFilter; /** * 配置密码加密 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 安全请求配置,这里配置的是Security的部分,请求全部通过,安全拦截在资源服务器配置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //配置允许的请求以及跨域问题 http .formLogin().loginPage("/login") .permitAll() .and().authorizeRequests().anyRequest().permitAll() .and().csrf().disable().cors() .and().addFilterAt(ignoreLogoutFilter, LogoutFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置获取用户信息以及加密方式 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { //配置忽略一些静态资源 web.ignoring().antMatchers("/css/**","/static/**"); } }
最后创建一个controller进行访问测试
@RestController @Slf4j public class UserController { @GetMapping("/user") public Object userInfo(Principal user, Authentication authentication) { log.info("user:{}",user); log.info("auth:{}", authentication); return user; } /** * 认证页面 * @return ModelAndView */ @GetMapping("/login") public ModelAndView require() { log.info("---认证页面---"); return new ModelAndView("ftl/login"); } @PreAuthorize("hasAuthority('ROLE_USER')") @GetMapping("/test/need_admin") public @ResponseBody String admin() { return "need_admin"; } }
带token的请求:
不带token的请求
带token,但是没有ROLE_ADMIN
权限
仔细一点的同学可能会发现,我这里使用的端口号与配置的不一样,这是因为我这里使用了网关进行了统一的入口访问,我们在我们之前的项目基础之上引入spring-cloud-starter-oauth2
依赖,并在application.yml配置文件加上如下配置:
##安全配置## security: oauth2: resource: user-info-uri: http://localhost:8766/user prefer-token-info: false
最后一步加上咱们的资源配置:
@Configuration @EnableResourceServer @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @Log4j2 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private UserInfoTokenServices userInfoTokenServices; @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 配置不需要安全拦截url .antMatchers("/test/no_need_token").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated(); } /** * 这个是跟服务绑定的,注意要跟client配置一致,如果客户端没有,则不能访问 * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources/*.resourceId(RESOURCE_ID)*/.stateless(true); resources.tokenServices(userInfoTokenServices); }
Fegin调用服务Token丢失
在微服务中我们经常会使用RestTemplate或Fegin来进行服务之间的调用,在这里就会出现一个问题,我们去调用别的服务的时候就会出现token丢失的情况,导致我们没有权限去访问。
所以我们需要加上一些拦截器将我们的token带着走。
针对fegin的配置代码如下:
public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); // 设置请求头 Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); requestTemplate.header(name, value); } } // 设置请求体,这里主要是为了传递 access_token Enumeration<String> parameterNames = request.getParameterNames(); StringBuilder body = new StringBuilder(); if (parameterNames != null) { while (parameterNames.hasMoreElements()) { String name = parameterNames.nextElement(); String value = request.getParameter(name); // 将 Token 加入请求头 if ("access_token".equals(name)) { requestTemplate.header("authorization", "Bearer " + value); } // 其它参数加入请求体 else { body.append(name).append("=").append(value).append("&"); } } } // 设置请求体 if (body.length() > 0) { // 去掉最后一位 & 符号 body.deleteCharAt(body.length() - 1); requestTemplate.body(body.toString()); } } }
然后将这个拦截器加入到我们fegin请求拦截器中:
@Configuration public class FeignRequestConfiguration { @Bean public RequestInterceptor requestInterceptor() { return new FeignRequestInterceptor(); } }
RestTemplate调用服务Token丢失
针对RestTemplate的我们拦截器可以这样写:
@Component public class TokenInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers = request.getHeaders(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)auth.getDetails(); // 加入自定义字段 headers.add("Authorization", details.getTokenType() + " " + details.getTokenValue()); // 保证请求继续被执行 return execution.execute(request, body); } }
并且同样的也设置到RestTemplate的拦截器中
@Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(tokenInterceptor)); return restTemplate; }
然后重启我们的项目,我们就可以成功调用到我们的服务啦。
总结
针对安全认证框架,常用的我们有Shiro,SpringSecurity。
个人认为Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
Shiro具有以下的有点:
-
易于理解的 Java Security API;
-
简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
-
对角色的简单的签权(访问控制),支持细粒度的签权;
-
支持一级缓存,以提升应用程序的性能;
-
内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
-
异构客户端会话访问;
-
非常简单的加密 API;
-
不跟任何的框架或者容器捆绑,可以独立运行。
Shiro三个核心组件:Subject, SecurityManager 和 Realms.
Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
Spring Security和Shiro
相同点:
1:认证功能
2:授权功能
3:加密功能
4:会话管理
5:缓存支持
6:rememberMe功能.......
不同点:
优点:
1:Spring Security基于Spring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发
2:Spring Security功能比Shiro更加丰富些,例如安全防护
3:Spring Security社区资源比Shiro丰富
缺点:
1:Shiro的配置和使用比较简单,Spring Security上手复杂
2:Shiro依赖性低,不需要任何框架和容器,可以独立运行,而Spring Security依赖于Spring容器
个人心得
本次学习个人感觉比较吃力,存有以下疑惑之处:
-
就是官网的资料有些比较深,难以挖掘,如何更快方式找到相应的技术
-
官网的资料都是一些较为片面的,怎么知道如何书写具体的配置文件
-
在配置文件中具体的方法又该如何重写,并且具体方法作用又是什么
-
最后一点还是更像知道第一个能够实现某个技术的牛人是怎么做到的
希望有人能够帮我解答一下。
古人云温故而知新可以为师矣
附录数据库表
-- used in tests that use HSQL create table oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); create table oauth_client_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication_id VARCHAR(256) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); create table oauth_access_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication_id VARCHAR(256) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256), authentication LONGVARBINARY, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication LONGVARBINARY ); create table oauth_code ( code VARCHAR(256), authentication LONGVARBINARY ); create table oauth_approvals ( userId VARCHAR(256), clientId VARCHAR(256), scope VARCHAR(256), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); -- customized oauth_client_details table create table ClientDetails ( appId VARCHAR(256) PRIMARY KEY, resourceIds VARCHAR(256), appSecret VARCHAR(256), scope VARCHAR(256), grantTypes VARCHAR(256), redirectUrl VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(256) );
参考资料
[Oauth2官网] https://oauth.net/2/
[阮一峰] http://www.ruanyifeng.com/blog/2019/04/oauth_design.html
[SpringBoot Oauth2] https://spring.io/guides/tutorials/spring-boot-oauth2/
[Spring Security Oauth] https://projects.spring.io/spring-security-oauth/docs/oauth2.html Spring Security Oauth2
[Spring Cloud Security] https://docs.spring.io/spring-cloud-security/docs/2.2.4.RELEASE/reference/html/ Spring Cloud Security