在微服务架构中,咱们有不少业务模块,每一个模块都须要有用户认证,权限校验。有时候也会接入来自第三方厂商的应用。要求是只登陆一次,便可在各个服务的受权范围内进行操做。看到这个需求,立马就想到了这不就是单点登陆吗?因而基于这样的需求,做者使用spring-cloud-oauth2去简单的实现了下用户认证和单点登陆。html
OAuth2是一个关于受权的网络标准,他定制了设计思路和执行流程。OAuth2一共有四种受权模式:受权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner password)和客户端模式(client credentials)。数据的全部者告诉系统赞成受权第三方应用进入系统,获取这些数据。因而数据全部者生产了一个短期内有效的受权码(token)给第三方应用,用来代替密码,供第三方使用。具体流程请看下图,具体的OAuth2介绍,能够参考这篇文章,写的很详细。(http://www.ruanyifeng.com/blog/2019/04/oauth_design.html)java
令牌(token)和密码(password)的做用是同样的,均可以进入系统获取资源,可是也有几点不一样:mysql
本篇介绍的是经过密码模式来实现单点登陆的功能。web
在微服务架构中,咱们的一个应用可能会有不少个服务运行,协调来处理实际的业务。这就须要用到单点登陆的技术,来统一认证调取接口的是哪一个用户。那总不能请求一次,就认证一次,这么作确定是不行的。那么就须要在认证完用户以后,给这个用户受权,而后发一个令牌(token),有效期内用户请求资源时,就只须要带上这个标识本身身份的token便可。redis
认证中心:oauth2-oauth-server,OAuth2的服务端,主要完成用户Token的生成、刷新、验证等。spring
微服务:mzh-etl,微服务之一,接收到请求以后回到认证中心(oauth2-oauth-server)去验证。sql
使用到的框架是java基础的spring boot 和spring-cloud-oauth2数据库
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
由于spring-cloud-starter-oauth2
中包含了spring-cloud-starter-security
,因此就不用再单独引入了,引入redis包是为了使用redis来存储token。json
这里主要用到的是redis的配置,mysql数据库的配置暂时没有用到。缓存
spring: application: name: oauth-server datasource: url: jdbc:mysql://localhost:3306/mzh_oauth?useSSL=false&characterEncoding=UTF-8 username: root password: admin123 driver-class-name: com.mysql.jdbc.Driver hikari: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 maximum-pool-size: 9 redis: database: 0 host: localhost port: 6379 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000 server: port: 8888 use-forward-headers: true management: endpoint: health: enabled: true
须要继承WebSecurityConfigurerAdapter
/** * @Author mzh * @Date 2020/10/24 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; /** * 修改密码的加密方式 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception{ return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ // 若是使用BCryptPasswordEncoder,这里就必须指定密码的加密类 auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/oauth/**").permitAll(); } }
BCryptPasswordEncoder
是一个不可逆的密码加密类,AuthenticationManager
是OAuth2的password必须指定的受权管理Bean。
CustomUserDetailsService
这个类是被注入进来的,熟悉spring security的同窗应该知道,spring security有一个本身的UserdetailsService
用于权限校验时获取用户信息,可是不少时候不符合咱们的业务场景,就须要重现实现这个类。
CustomUserDetailsService
UserDetailsService
这个类的核心方法就是loadUserByUsername()
方法,他接收一个用户名,返回一个UserDetails
对象。
/** * @Author mzh * @Date 2020/10/24 */ @Component(value = "customUserDetailsService") public class CustomUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据username 去数据库查询 user // 2.获取用户的角色和权限 // 下面是写死,暂时不和数据库交互 if(!(("admin").equals(username))){ throw new UsernameNotFoundException("the user is not found"); }else{ String role = "ADMIN_ROLE"; List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role)); String password = passwordEncoder.encode("123456"); return new User(username,password,authorities); } } }
这里是在程序中写死了用户和权限。帐号:admin,密码:123456,权限:ADMIN_ROLE(注意是权限,不是角色),实际中应该从数据库获取用户和相关的权限,而后进行认证。
OAuth2配置须要继承AuthorizationServerConfigurerAdapter
类
/** * @Author mzh * @Date 2020/10/24 */ @Configuration @EnableAuthorizationServer public class Oauth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserDetailsService customUserDetailsService; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore redisTokenStore; /** * 对AuthorizationServerEndpointsConfigurer参数的重写 * 重写受权管理Bean参数 * 重写用户校验 * 重写token缓存方式 * @param endpointsConfigurer * @throws Exception */ @Override public void configure(final AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception{ endpointsConfigurer.authenticationManager(authenticationManager) .userDetailsService(customUserDetailsService) .tokenStore(redisTokenStore); } /** * 客户端的参数的重写 * 这里是将数据直接写入内存,实际应该从数据库表获取 * clientId:客户端Id * secret:客户端的密钥 * authorizedGrantTypes:受权方式 * authorization_code: 受权码类型, * implicit: 隐式受权, * password: 密码受权, * client_credentials: 客户端受权, * refresh_token: 经过上面4中方式获取的刷新令牌获取的新令牌, * 注意是获取token和refresh_token以后,经过refresh_toke刷新以后的令牌 * accessTokenValiditySeconds: token有效期 * scopes 用来限制客户端访问的权限,只有在scopes定义的范围内,才能够正常的换取token * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception{ clients.inMemory() .and() .withClient("mzh-etl") .secret(passwordEncoder.encode("mzh-etl-8888")) .authorizedGrantTypes("refresh_token","authorization_code","password") .accessTokenValiditySeconds(3600) .scopes("all"); } @Override public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfigurer) throws Exception{ serverSecurityConfigurer.allowFormAuthenticationForClients(); serverSecurityConfigurer.checkTokenAccess("permitAll()"); serverSecurityConfigurer.tokenKeyAccess("permitAll()"); serverSecurityConfigurer.passwordEncoder(passwordEncoder); } }
上述步骤完成以后启动服务,而后观察IDEA下方的Endpoints中的Mappings,就能够找到相关的认证端口。主要的有如下几个:
POST /oauth/authorize 受权码模式认证受权接口 GET/POST /oauth/token 获取 token 的接口 POST /oauth/check_token 检查 token 合法性接口
到此,认证中心就算是建立完成了。咱们经过idea的REST Client 来请求一个token进行测试。
请求内容以下:
POST http://localhost:8888/oauth/token?grant_type=password&username=admin&password=123456&scope=all Accept: */* Cache-Control: no-cache Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
第一行POST http://localhost:8888/oauth/token?grant_type=password&username=admin&password=123456&scope=all
表示发起一个POST请求,请求路径是/oauth/token
,请求参数是grant_type=password
表示认证类型是password,username=admin&password=123456
表示用户名是admin
,密码是123456
,scope=all
是权限相关的,以前在Oauth2Config
中配置了scope是all。
第四行表示在请求头中加入一个字段Authorization
,值为Basic空格base64(clientId:clientSecret),咱们以前配置的clientId是“meh-etl”,clientSecret是"meh-etl-8888",因此这个值的base64是:bXpoLWV0bDptemgtZXRsLTg4ODg=
。
运行请求以后,若是参数都正确的话,获取到返回的内容以下:
{ // token值,后面请求接口时都须要带上的token "access_token": "b4cb804c-93d2-4635-913c-265ff4f37309", // token的形式 "token_type": "bearer", // 快过时时能够用这个换取新的token "refresh_token": "5cac05f4-158f-4561-ab16-b06c4bfe899f", // token的过时时间 "expires_in": 3599, // 权限范围 "scope": "all" }
token值过时以后,能够经过refresh_token来换取新的access_token
POST http://localhost:8888/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282 Accept: */* Cache-Control: no-cache Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
此次grant_type
的值为“refresh_token”,refresh_token
的值是要过时的token的refresh_token值,也就是以前请求获取Token的refresh_token值,请求以后会返回一个和获取token时同样格式的数据。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
spring: application: name: mzh-etl redis: database: 1 host: localhost port: 6379 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 10000 server: port: 8889 security: oauth2: client: # 须要和以前认证中心配置中的同样 client-id: mzh-etl client-secret: mzh-etl-8888 # 获取token的地址 access-token-uri: http://localhost:8888/oauth/token resource: id: mzh-etl user-info-uri: user-info authorization: # 检查token的地址 check-token-access: http://localhost:8888/oauth/check_token
这里的配置必定要仔细,必须和以前认证中心中配置的同样。
在OAuth2中接口也称为资源,资源的权限也就是接口的权限。spring-cloud-oauth2提供了关于资源的注解@EnableResourceServer
/** * @Author mzh * @Date 2020/10/24 */ @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${security.oauth2.client.client-id}") private String clientId; @Value("${security.oauth2.client.client-secret}") private String clientSecret; @Value("${security.oauth2.authorization.check-token-access}") private String checkTokenEndpointUrl; @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean("redisTokenStore") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Bean public RemoteTokenServices tokenService() { RemoteTokenServices tokenService = new RemoteTokenServices(); tokenService.setClientId(clientId); tokenService.setClientSecret(clientSecret); tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl); return tokenService; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenServices(tokenService()); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/get/**").authenticated(); } }
@RestController public class UserController { @GetMapping("get") @PreAuthorize("hasAuthority('ADMIN_ROLE')") public Object get(Authentication authentication){ authentication.getAuthorities(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); String token = details.getTokenValue(); return token; } }
这个接口就是会返回一个请求他时携带的token值,@PreAuthorize
会在请求接口时检查是否用权限“ADMIN_ROLE”(以前认证中心配置的权限)
启动服务,只有当用户有“ADMIN_ROLE“的时候,才能正确返回,不然返回401未受权
一样适用REST Client来发起一个请求:
GET http://localhost:8889/get Accept: */* Cache-Control: no-cache Authorization: bearer b4cb804c-93d2-4635-913c-265ff4f37309
请求路径是http://localhost:8889/get 而后在请求头部带上咱们上一步骤获取到的token,放入到Authorization
中,格式是bearer空格token值,若是请求成功,就会把token原样返回。
本文由博客群发一文多发等运营工具平台 OpenWrite 发布