首先配置一下Nacos,参考https://blog.csdn.net/weixin_43917045/article/details/132852850
<!-- SpringSession Redis支持 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 添加Redis的Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用SpringSecurity框架作为权限校验框架:-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
三个服务都需要添加以上依赖。
然后给三个服务都修改配置文件。
这样,默认情况下每个服务的接口都会被SpringSecurity所保护,只有登录成功后,才可以被访问。
启动nacos和三个服务。
此时访问borrow借阅接口http://localhost:8201/borrow/1
会自动跳转到登陆接口http://localhost:8201/login
点击登录后进入redis-cli可以看到用户密码已存在。
此时访问book服务,可以看到直接能调用,不用登录,因为上次登录在borrow服务访问中已经存入账户密码信息。
此时先退出登录。
退出账户后访问http://localhost:8301/book/1
就会跳转到登录页面http://localhost:8301/book/1
。
然后再进行登录,进行借阅接口访问,发现出错,是由于borrow服务调用user和book服务是远程调用形式,,而这种形式没有进行任何验证。出现这种情况原因是RestTemplate远程调用的时候,由于请求没有携带Session的Cookies,所以导致验证失败,访问不成功,返回401。因此存在不便。
还原不带任何spring cloud alibaba依赖的项目,
加入spring cloud依赖
创建auth-service(左上角新增moudle)并导入依赖。(图片中服务名称a打成o了)
编写配置类
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() //
.and()
.formLogin().permitAll(); //使用表单登录
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
auth
.inMemoryAuthentication() //直接创建一个用户,懒得搞数据库了
.passwordEncoder(encoder)
.withUser("test").password(encoder.encode("123456")).roles("USER");
}
@Bean //这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
@EnableAuthorizationServer //开启验证服务器
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
@Resource
private AuthenticationManager manager;
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
/**
* 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端,
* 之后这些指定的客户端就可以按照下面指定的方式进行验证
* @param clients 客户端配置工具
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory() //这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取
.withClient("web") //客户端名称,随便起就行
.secret(encoder.encode("654321")) //只与客户端分享的secret,随便写,但是注意要加密
.autoApprove(false) //自动审批,这里关闭,要的就是一会体验那种感觉
.scopes("book", "user", "borrow") //授权范围,这里我们使用全部all
.authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
//授权模式,一共支持5种,除了之前我们介绍的四种之外,还有一个刷新Token的模式
//这里我们直接把五种都写上,方便一会实验,当然各位也可以单独只写一种一个一个进行测试
//现在我们指定的客户端就支持这五种类型的授权方式了
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.passwordEncoder(encoder) //编码器设定为BCryptPasswordEncoder
.allowFormAuthenticationForClients() //允许客户端使用表单验证,一会我们POST请求中会携带表单信息
.checkTokenAccess("permitAll()"); //允许所有的Token查询请求
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(manager);
//由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式
}
}
然后启动auth-service。
使用postman进行接口测试。
客户端模式只需要提供id和secret即可直接拿到token,但还需要添加一个grant_type表面我们的授权方式,默认请求路径为http://localhost:8500/sso/oauth/token
通过访问http://localhost:8500/sso/oauth/check_token
来验证token是否有效
密码模式还需要提供具体的用户名和密码,授权模式定义为password即可。
还需要在请求头中添加Basic验证信息,直接写id和secret即可
这种模式需要在验证服务器上进行登录操作,而不是直接请求Token,验证登陆地址http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=token
。
response_type一定是token类型,这样才会返回Token。
请求上边的验证登陆地址则会进入登录页面。
登录之后出现错误,这是因为登陆后验证服务器需要将结果返回给客户端,所以需要提供客户端的回调地址,这样浏览器会被重定向到指定的回调地址并且请求中回携带Token信息,此时随便配置一个回调地址,
配置回调地址,然后重启
此时会要求进行授权,
这种方式和1.2.3的流程一样,但是请求的地址是code类型:http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=code
,访问该地址,因为在1.2.3已经登录过了,可以看到访问之后,依然会进入到回调地址,但是此时给的是授权码code了,不是直接给token。但是有时候可能需要token。
按照2.1之前的第四个授权码图示原来,需要携带授权码
和secret
一起请求,才能拿到token,正常情况下是由回调的服务器进行处理,在postman中测试进行,复制刚得到的授权码,接口请求localhost:8500/sso/oauth/token
可以看到正常拿到token
以上四种基本的Token请求方式。
当Token过期时,就可以使用这个refresh_token来申请一个新的Token。
重新请求
改为点击发送,出现异常
查看日志发现,还需要单独配置一个UserDetailsService,我们直接把Security中的实例注册为Bean。
然后再Endpoint中设置
添加完重启,重启之后token就没有了,需要重新申请以下。
访问http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=code
,先进行登录test,123456,然后授权获得code
前面已经将服验证服务器搭建完成了,下面实现单点登录,SpringCloud提供了客户端的直接实现,只需要添加一个注解和少量配置即可将我们的服务作为一个单点登录应用,使用的是第四张授权码模式。
这种模式只是将验证方式由原来的默认登陆形式改变为了统一在授权服务器登录的形式。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
在book-service中添加依赖
启动类添加注解
然后需要在配置文件中配置验证服务器相关的信息:
security:
oauth2:
client:
#不多说了
client-id: web
client-secret: 654321
#Token获取地址
access-token-uri: http://localhost:8500/sso/oauth/token
#验证页面地址
user-authorization-uri: http://localhost:8500/sso/oauth/authorize
resource:
#Token信息获取和校验地址
token-info-uri: http://localhost:8500/sso/oauth/check_token
启动book服务
访问,由于book服务是8301端口,需要将重定向地址也改为8301
然后会登录跳转到授权页面
授权后访问成功
此时访问成功了但,但是用户信息是否也一并保存过来了,直接获取以下SpringSecurity的Context查看用户信息,获取方式和之前一样
再次访问book服务,输出用户信息
接着将所有的服务都使用这种方式进行验证,将重定向地址给所有服务都加上
将borrow-service,user-service都加上依赖,配置文件内容,和启动类的注释。
配置完启动user和borrow服务和auth服务
此时访问user服务和book服务都是授权后可登录。但是此时三个服务session会互相占用,若访问完user服务,再访问user服务不会再次验证,但是若再访问book或其他服务就会需要再次验证。
这里有两个方案:一是和之前一样Session统一存储。二是设置context-path路径,每个服务单独设置,就不会互相占用了。
前面已经实现了将服务作为单点登录应用直接实现单点登录。但是如果是第三方访问,则我们就需要将服务作为资源服务了,作为资源服务就不会再提供验证的过长,而是直接要求请求时携带Token,而验证过程使用postman进行测试。
总之,与上边实现的相比,下边实现的访问过程只需要携带Token就能访问这些资源服务器了,客户端被独立了出来,用于携带Token去访问这些服务。
此时需要添加注解和少量配置
修改完配置和注解重启book-service,访问book服务,显示没有权限访问资源。
这是由于请求头中没有携带token信息,有两种方式可访问到资源
一:在URL后添加 access_token 请求参数,值为Token值
二:在请求头中添加 Authorization 值为 Bearer + Token值
在URL后添加 access_token 请求参数
此时通过密码模式访问拿到token
后缀参数加上获取的access_token即可访问
在请求头中添加 Authorization 值为 Bearer + Token值
直接访问没有权限
然后可访问资源
到此,资源服务器就搭建完成
编写一个配置类,使得用户授权了某个Scope才可以访问此服务。例如,必须有book作用域才能访问book-service服务
@Configuration
public class ResourceConfiguration extends ResourceServerConfigurerAdapter { //继承此类进行高度自定义
@Override
public void configure(HttpSecurity http) throws Exception { //这里也有HttpSecurity对象,方便我们配置SpringSecurity
http
.authorizeRequests()
.anyRequest().access("#oauth2.hasScope('lbwnb')"); //添加自定义规则
//Token必须要有我们自定义scope授权才可以访问此资源
}
}
添加完重启book-service
访问发现作用域不足
重启后访问正常
实际上资源服务器完全没有必要将Security的信息保存在Session中,因为现在只需要将Token告诉资源服务器,那么资源服务器就可以联系验证服务器得到用户信息,不需要使用之前的Session存储机制了,所以会发现HttpSession中没有
SPRING_SECURITY_CONTEXT
,现在Security信息都是通过连接资源服务器获取。
但是目前每次访问都需要去验证一次,浪费资源,这种可以通过JTW去解决。
接着将所有服务都改为基于@EnableResourceServer方式实现,borrow-service和user-service的配置文件,启动类的注解都要修改
然后启动user、book、borrow服务,此时这三个服务都是资源服务。
访问borrow服务还是会出问题,因为远程调用也需要携带token,但是远程调用过程没有携带任何token’信息。因此需要想办法将用户传来的token信息在进行远程调用同时也携带上。因此可以直接使用OAuth2RestTemplate,它会在请求其他服务时携带当前请求的Token信息,它继承自RestTemplate,直接定义一个Bean
@Configuration
public class WebConfiguration {
@Resource
OAuth2ClientContext context;
@Bean
public OAuth2RestTemplate restTemplate(){
return new OAuth2RestTemplate(new ClientCredentialsResourceDetails(), context);
}
}
修改后重启borrow,然后访问borrow服务,此时正常调用。这样远程调用的时候就会也携带token信息
加入依赖
## 给最外层pom添加
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
## 给每个服务添加
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
配置Nacos服务
配置完启动nacos
三个服务分别添加配置
修改borrow服务service中的调用方式
配置完启动三个服务,进入nacos查看
访问borrow服务,此时已经加入了nacos、负载均衡。其中主要是要加入依赖和在WebConfiguration方法上加入@LoadBalanced注解
@FeignClient("book-service")
public interface BookClient {
@RequestMapping("/book/{bid}")
Book getBookById(@PathVariable("bid") int bid);
}
@FeignClient("user-service")
public interface UserClient {
@RequestMapping("/user/{uid}")
User getUserById(@PathVariable("uid") int uid);
}
在borrowServiceImpl直接注入
启动类加入注解
此时openfeign访问没有携带token。还会出现之前的访问borrow服务出错。此时需要添加配置。
feign:
oauth2:
#开启Oauth支持,这样就会在请求头中携带Token了
enabled: true
#同时开启负载均衡支持
load-balanced: true
重新启动borrow-service,此时接口调用带有token,则访问成功
但是每次访问一次就需要校验一次,若用户多了,服务器压力就很大。
public void test(){
String str = "你们可能不知道只用20万赢到578万是什么概念";
//Base64不只是可以对字符串进行编码,任何byte[]数据都可以,编码结果可以是byte[],也可以是字符串
String encodeStr = Base64.getEncoder().encodeToString(str.getBytes());
System.out.println("Base64编码后的字符串:"+encodeStr);
System.out.println("解码后的字符串:"+new String(Base64.getDecoder().decode(encodeStr)));
}
@Bean
public JwtAccessTokenConverter tokenConverter(){ //Token转换器,将其转换为JWT
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("lbwnb"); //这个是对称密钥,一会资源服务器那边也要指定为这个
return converter;
}
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter converter){ //Token存储方式现在改为JWT存储
return new JwtTokenStore(converter); //传入刚刚定义好的转换器
}
@Resource
TokenStore store;
@Resource
JwtAccessTokenConverter converter;
private AuthorizationServerTokenServices serverTokenServices(){ //这里对AuthorizationServerTokenServices进行一下配置
DefaultTokenServices services = new DefaultTokenServices();
services.setSupportRefreshToken(true); //允许Token刷新
services.setTokenStore(store); //添加刚刚的TokenStore
services.setTokenEnhancer(converter); //添加Token增强,其实就是JwtAccessTokenConverter,增强是添加一些自定义的数据到JWT中
return services;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenServices(serverTokenServices()) //设定为刚刚配置好的AuthorizationServerTokenServices
.userDetailsService(service)
.authenticationManager(manager);
}
重启auth-service服务,通过密码模式获取token,此时返回的token是JWT令牌。可对其进行Base64解码
生成的access_token用base64解码查看,需要在".“前后分开解码。
第一段为jwt
第二段用户信息
security:
oauth2:
resource:
jwt:
key-value: lbwnb #注意这里要跟验证服务器的密钥一致,这样算出来的签名才会一致
然后在book-service,user-service都进行以上相应的修改,修改完重启对应服务,并加上token进行访问
user,book服务访问正常