资源服务器授权以及OAuth2对接微服务
1. 资源服务器授权配置
1.1 资源服务授权配置
基本上所有微服务都是资源服务。
1. 资源服务中配置公钥:
- 认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使用公钥来校验令牌的合法性。 将公钥拷贝到 public.key 文件中,将此文件拷贝到每一个需要的资源服务工程的classpath(resources目录)下,eg: 用户微服务:
2. 资源服务中添加依赖:
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
|
3. 配置每个系统的Http请求路径安全控制策略以及读取公钥信息识别令牌,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
@Configuration @EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String PUBLIC_KEY = "public.key";
@Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); }
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; }
private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } }
@Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers( "/user/add","/user/load/*"). permitAll() .anyRequest(). authenticated(); } }
|
1.2 用户微服务资源授权
1.将生成的公钥public.key拷贝到changgou-service-user微服务工程的resources目录下,如下图:
2.引入依赖:
- 在changgou-service-user微服务工程pom.xml中引入上面的oauth2.0依赖
3.资源授权配置:
- 在changgou-service-user工程中创建com.changgou.user.config.ResourceServerConfig(上边有)
1.3 授权测试
用户每次访问微服务的时候,需要先申请令牌,令牌申请后,每次将令牌放到头文件中,才能访问微服务。
头文件中每次需要添加一个Authorization
头信息,头信息的内容格式为:bearer token
(token为申请得到的令牌)。
1.不携带令牌测试:
访问http://localhost:8089/user 不携带令牌,结果如下:
2.携带正确令牌访问
先通过认证微服务执行登录,拿到令牌:
访问localhost:9001/user/login?username=szitheima&password=szitheima获取token令牌,结果如下:
访问http://localhost:8089/user/szitheima(携带正确令牌),根据username查询用户信息
执行后结果如下:
3.携带错误令牌
访问http://localhost:18089/user/szitheima携带不正确的token令牌,结果如下:
2. OAuth2对接微服务
用户每次访问微服务的时候,先去oauth2.0服务登录,登录后再访问微服务网关,微服务网关将请求转发给其他微服务处理。
步骤:
- 1.用户登录成功后,会将令牌信息存入到cookie中(一般建议存入到头文件中)
- 2.用户携带Cookie中的令牌访问微服务网关
- 3.微服务网关先获取头文件中的令牌信息,如果Header中没有Authorization令牌信息,则去参数中找,参数中如果没有,则去Cookie中找Authorization,最后将令牌信息封装到Header中,然后再调用其他微服务
- 4.其他微服务会获取头文件中的Authorization令牌信息,然后匹配令牌数据是否能使用公钥解密,如果解密成功说明用户已登录,解密失败,说明用户未登录
修改changgou-gateway-web的全局过滤器com.changgou.filter.AuthorizeFilter,实现将令牌信息添加到头文件中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
|
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { private static final String AUTHORIZE_TOKEN = "Authorization";
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String path = request.getURI().getPath(); if (path.startsWith("/api/user/login") || path.startsWith("/api/brand/search/")) { return chain.filter(exchange); } String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN); boolean hasToken = true; if(StringUtils.isEmpty(token)){ HttpCookie first = request.getCookies().getFirst(AUTHORIZE_TOKEN); if(first!=null){ token=first.getValue(); } hasToken = false; } if(StringUtils.isEmpty(token)){ token= request.getQueryParams().getFirst(AUTHORIZE_TOKEN); hasToken = false; } if(StringUtils.isEmpty(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } try { if (!hasToken) { if (!token.startsWith("bearer") && !token.startsWith("Bearer")) { token = "bearer" + token; } request.mutate().header(AUTHORIZE_TOKEN, token); } } catch (Exception e) { e.printStackTrace(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); }
@Override public int getOrder() { return 0; } }
|
访问测试:
访问http://localhost:8001/api/user/szitheima
,将生成的新令牌放到头文件中,在令牌前面添加Bearer
,这里主要由个空格,效果如下:
2.2 用户身份权限控制
由于我们项目使用了微服务,任何用户都有可能使用任意微服务,此时我们需要控制相关权限,例如:普通用户角色不能使用用户的删除操作,只有管理员才可以使用,那么这个时候就需要使用到SpringSecurity的权限控制功能了。
2.2.1 角色身份定义
在changgou-user-oauth2认证微服务中,com.changgou.oauth.config.UserDetailsServiceImpl该类实现了加载用户相关信息,如下代码:
上述代码给登录用户定义了2个角色,分别为user
和 admin
,这一块我们目前使用的是硬编码方式将角色写死了,后面会从数据库加载。
2.2.2 角色权限控制
在每个微服务中,需要获取用户的角色,然后根据角色识别是否允许操作指定的方法,Spring Security中定义了四个支持权限控制的表达式注解,分别是@PreAuthorize
、@PostAuthorize
、@PreFilter
和@PostFilter
。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。在需要控制权限的方法上,我们可以添加@PreAuthorize
注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法。
1.开启@PreAuthorize
在changgou-user-service的ResourceServerConfig类上添加@EnableGlobalMethodSecurity
注解,用于开启@PreAuthorize
的支持,代码如下:
2.方法权限控制
在changgoug-service-user微服务的com.changgou.user.controller.UserController类的findAll()
方法(查询所有用户信息)上添加权限控制注解@PreAuthorize
,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@PreAuthorize("hasAnyRole('admin')") @GetMapping public Result<List<User>> findAll() { List<User> list = userService.findAll(); return new Result<List<User>>(true, StatusCode.OK, "查询成功", list); }
|
3.测试用户角色权限
访问localhost:8001/api/user获取所有用户信息:
发现上面请求无法访问,因为用户登录的时候,角色不包含admin角色,而findAll()
方法需要admin角色,所以被拦截了。
接下来再测试其他没有加admin管理员权限的方法,其他方法(eg: findById()
)没有配置拦截,所以用户登录后就会放行:
知识点说明:
如果希望一个方法能被多个角色访问,配置:@PreAuthorize("hasAnyAuthority('admin','user')")
如果希望一个类都能被多个角色访问,在类上配置:@PreAuthorize("hasAnyAuthority('admin','user')")
3. OAuth2动态加载数据(从数据库获取数据)
前面OAuth2我们用的数据都是静态的,在现实工作中,数据都是从数据库加载的,所以我们需要调整一下OAuth服务,从数据库加载相关数据。
- 客户端数据[生成令牌相关数据]
- 用户登录账号密码从数据库加载
3.1 客户端信息相关数据加载
3.1.1 数据介绍
客户端静态数据:
在changgou-user-oauth
的com.changgou.oauth.config.AuthorizationServerConfig类中配置了客户端静态数据,主要用于配置客户端数据,代码如下:
客户端表结构:
创建一个数据库changgou_oauth
,并在数据库中创建一张表,表主要用于记录客户端相关信息,表结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用', `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密算法加密', `scope` varchar(256) DEFAULT NULL COMMENT '对应的范围', `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式', `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址', `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期', `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期', `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
字段说明:
1 2 3 4 5 6 7
| client_id:客户端id resource_ids:资源id(暂时不用) client_secret:客户端秘钥 scope:范围 access_token_validity:访问token的有效期(秒) refresh_token_validity:刷新token的有效期(秒) authorized_grant_type:授权类型:authorization_code,password,refresh_token,client_credentials
|
导入2条记录到表中,SQL如下:数据中密文分别为changgou、szitheima
:
1 2
| INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$wZRCFgWnwABfE60igAkBPeuGFuzk74V2jw3/trkdUZpnteCtJ9p9m', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null); INSERT INTO `oauth_client_details` VALUES ('szitheima', null, '$2a$10$igxoCZxTbjWx5TrmfWEEpe/WFdwbUhbxik9BKTe9i64ZOSfnu/lqe', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null);
|
上述表结构属于SpringSecurity Oauth2.0所需的一个认证表结构,不能随意更改。相关操作在其他类中有所体现,如:org.springframework.security.oauth2.provider.client.JdbcClientDetailsService
中的片段代码如下:
3.1.2 从库数据加载相关数据
修改连接配置:
从数据库加载数据,我们需要先配置数据库连接,在changgou-user-oauth2的application.yml中配置连接信息,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| server: port: 9001
spring: application: name: user-auth redis: database: 0 host: 8.131.66.136 port: 6379 password: csp19990129 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/changgou_oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC username: root password: root main: allow-bean-definition-overriding: true
eureka: instance: prefer-ip-address: true client: service-url: defaultZone: http://127.0.0.1:7001/eureka
auth: ttl: 3600 clientId: changgou clientSecret: changgou cookieDomain: localhost cookieMaxAge: -1
encrypt: key-store: location: classpath:/changgou.jks secret: changgou alias: changgou password: changgou
|
修改客户端加载源:
修改changgou-user-oauth2的com.changgou.oauth.config.AuthorizationServerConfig类的configure方法,将之前静态的客户端数据变成从数据库加载,修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).clients(clientDetails()); }
|
UserDetailsServiceImpl修改:
将之前的加密方式去掉即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username); if (clientDetails != null) { String clientSecret = clientDetails.getClientSecret(); return new User( username, clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList("")); } } ... }
|
3.1.3 测试
授权码模式测试:
访问:http://localhost:9001/oauth/authorize?client_id=szitheima&response_type=code&scop=app&redirect_uri=http://localhost
效果如下:
用户名对应应用id,密码对应秘钥。账号输入:szitheima
密码:szitheima
,效果如下:
密码模式授权测试:
我们之前编写的账号密码登录代码如下,每次都会加载指定的客户端ID和指定的秘钥,所以此时的客户端ID和秘钥固定了,输入的账号密码不再是客户端ID和秘钥了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
@RequestMapping("/login") public Result<Map> login(String username, String password) { if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { throw new RuntimeException("用户名/密码不允许为空"); } AuthToken authToken = loginService.login(username, password, clientId, clientSecret, GRAND_TYPE); this.saveCookie(authToken.getAccessToken()); return new Result<>(true, StatusCode.OK, "令牌生成成功", authToken); } private void saveCookie(String token) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false); }
|
OAuth中的com.changgou.oauth.config.UserDetailsServiceImpl配置如下:
用户每次输入账号和密码,只要密码是szitheima,即可登录成功。
访问地址http://localhost:9001/user/login
输入账号密码均为szitheima,效果如下:
3.2 用户相关数据加载
因为我们目前整套系统是对内提供登录访问,所以每次用户登录的时候oauth需要调用用户微服务查询用户信息,如上图:
我们需要在用户微服务中提供用户信息查询的方法,并在oauth中使用feign调用即可。
在真实工作中,用户和管理员对应的oauth认证服务器会分开,网关也会分开,我们今天的课堂案例只实现用户相关的认证即可。
1.Feign创建
在changgou-service-user-api中创建com.changgou.user.feign.UserFeign,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@FeignClient(value = "changgou-user") @RequestMapping(value = "/user") public interface UserFeign {
@GetMapping({ "/load/{id}"}) public Result<User> findById(@PathVariable String id); }
|
2.修改UserController
修改changgou-service-user的UserController的findById方法,添加一个新的地址,用于加载用户信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
|
@GetMapping({ "/{id}","/load/{id}"}) public Result<User> findById(@PathVariable String id) { User user = userService.findById(id); return new Result<User>(true, StatusCode.OK, "查询成功", user); }
|
3.放行查询用户方法
因为oauth需要调用查询用户信息,需要在changgou-service-user中放行/user/load/{id}
方法,修改ResourceServerConfig,添加对/user/load/{id}
的放行操作,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers( "/user/add","/user/load/*"). permitAll() .anyRequest(). authenticated(); }
|
4.oauth调用查询用户信息
oauth认证微服务引入对changgou-user-api的依赖
1 2 3 4 5
| <dependency> <groupId>com.changgou</groupId> <artifactId>changgou-service-user-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
|
修改oauth的com.changgou.oauth.config.UserDetailsServiceImpl
的loadUserByUsername
方法,调用UserFeign查询用户信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if (StringUtils.isEmpty(username)) { return null; }
Result<com.changgou.user.pojo.User> userResult = userFeign.findById(username);
if (userResult == null || userResult.getData() == null) { return null; }
String pwd = userResult.getData().getPassword();
String permissions = "user,admin"; UserJwt userDetails = new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
|
完整的UserDetailsServiceImpl代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired ClientDetailsService clientDetailsService; @Autowired private UserFeign userFeign;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username); if (clientDetails != null) { String clientSecret = clientDetails.getClientSecret(); return new User( username, clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList("")); } } if (StringUtils.isEmpty(username)) { return null; } Result<com.changgou.user.pojo.User> userResult = userFeign.findById(username); if (userResult == null || userResult.getData() == null) { return null; } String pwd = userResult.getData().getPassword(); String permissions = "user,admin"; UserJwt userDetails = new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions)); return userDetails; } }
|
5.主启动类Feign开启
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@SpringBootApplication @EnableDiscoveryClient
@EnableFeignClients("com.changgou.user.feign") public class OAuthApplication { public static void main(String[] args) { SpringApplication.run(OAuthApplication.class, args); } @Bean(name = "restTemplate") public RestTemplate restTemplate() { return new RestTemplate(); } }
|
6.测试
我们换个数据库中的账号密码登录,分别输入zhangsan,效果如下: