1、Oauth2.0密码模式
在SpringCloud项目里 ,Oauth2.0密码模式 校验权限的总体流程还是 一样的。
只不过使用 密码模式获取 token,也就是说在获取 token 过程中必须带上用户的用户名和密码,获取到 的 token 是跟用户绑定的。
2、认证服务器搭建
客户端信息和用户信息 既可以存在内存里,也可以存在 数据库里, 存在内存的方式 我们已经在 上一篇 客户端模式 演示过了,接下来 密码模式就 看下 如何在数据库 存储。
2.1、表结构
2.2.1、Oauth2.0表结构:
可以直接从官网上扒下来,是Oauth2.0用来权限校验 预设的表。主要用来存token,授权码和客户端信息。
1、用户token表
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(48) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
2、客户端信息表
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`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
插入 micro-order 客户端记录, 密钥为 123456的 {bcrypt}+ bcrypt加密后的 密文
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('micro-order', 'micro-order', '{bcrypt}$2a$10$8HBBphskF43dijSHs8KQg./BWUnxeqRaFr0jbDwCcqJo0FNM6YZn2', 'all,read,writr,aa', 'client_credentials,refresh_token,password', NULL, 'oauth2', NULL, NULL, NULL, NULL);
3、客户端token
CREATE TABLE `oauth_client_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(48) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
4、授权码表
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
5、刷新token表
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
6、这个表不知道干嘛
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`lastModifiedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
2.2.2、用户角色表
用户信息包括角色权限 和我们的业务有关,由我们自己创建, 只需要向认证服务器框架提供 获取用户 信息 的 方法即可。
经典 用户 , 角色,用户_角色 关系表
1、用户表
最重要是用户名和密码
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
插入用户信息,用户名为 admin,密码为123456的 {bcrypt} + bcrypt加密后的密文
INSERT INTO `user`(`id`, `username`, `password`) VALUES (1, 'admin', '{bcrypt}$2a$10$8HBBphskF43dijSHs8KQg./BWUnxeqRaFr0jbDwCcqJo0FNM6YZn2');
2、角色表
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
插入角色 ROLE_ADMIN,注意一定要 ROLE_ 为前缀。不然会报错
INSERT INTO `role`(`id`, `name`) VALUES (1, 'ROLE_ADMIN');
3、用户角色关系表
CREATE TABLE `user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2、pom依赖
和客户端模式的依赖相同,由于需要 与数据库交互, 多引入了 数据持久化框 之类的依赖
<dependencies>
<!-- 注册到注册中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<!--数据库操作相关, 认证服务器 需要存储token,获取用户信息,客户端信息,这里采用myql存储 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2.3、代码配置
2.3.1、认证服务配置
@Configuration
// 开启认证服务器
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private UserServiceDetail userServiceDetail;
@Autowired
private ClientDetailsService clientDetailsService;
static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean // 声明 ClientDetails实现,用数据库存储,需要配置数据源对象
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// redisTokenStore
// endpoints.tokenStore(new MyRedisTokenStore(redisConnectionFactory))
// .authenticationManager(authenticationManager)
// .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
// 存数据库
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userServiceDetail);
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
//支持refreshtoken
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds(60 * 5);
//重复使用
tokenServices.setReuseRefreshToken(false);
tokenServices.setRefreshTokenValiditySeconds(60 * 10);
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
2.3.2、安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 密码加密方式
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers().anyRequest()
.and()
.authorizeRequests()
// .antMatchers("/oauth/**").permitAll();
.antMatchers("/").authenticated();
/* http.csrf().disable().exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests().
antMatchers("/favicon.ico").permitAll()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.and()
.httpBasic().disable();*/
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); //拦截所有请求 通过httpBasic进行认证
}
}
2.3.3、UserServiceDetail
定义了如何获取用户信息,比如用户名,密码,角色权限等,这里我们用jpa从数据库查询,也就是user,role,user_role根据用户名查询出 用户信息 交由权限框架 校验。
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
2.3.4、token校验接口
下游服务 调认证服务器的这个接口来验证token是否有效,token有效的话 则返回 认证后的用户信息, 让下游服务 做更细致的权限控制,比如方法级别的控制。
@Slf4j
@RestController
@RequestMapping("/security")
public class SecurityController {
@RequestMapping(value = "/check", method = RequestMethod.GET)
public Principal getUser(Principal principal) {
log.info(principal.toString());
return principal;
}
}
2.4、配置文件
spring.application.name=oauth-pwd-server
#spring.cloud.controller.uri= http://localhost:9009/
server.port=9052
#eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
# 数据源配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://*****:3306/oauth2.0?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
logging.level.org.springframework.security=debug
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.security.basic.enabled=true
2.5、启动类
@SpringBootApplication
// 认证服务器本身也作为一种 受保护资源
@EnableResourceServer
public class OauthPwdServerApplication {
public static void main(String[] args) {
SpringApplication.run(OauthPwdServerApplication.class, args);
}
}
3、向认证服务器获取token
获取token的 接口仍然是 /oauth/token
3.1、Authorization请求头
在密码模式中 ,Authorization请求头需要填写 加密后的 客户端id和客户端密钥, 只能 用来校验 客户端 是否合法。
用postman,选择Basic Auth加密方式
这里的客户端id和 密码对应 我们一开始 保存到 oauth_client_details 表里的客户端记录
数据库里存的密文, 页面上填明文。
之后就可以看到在请求头里 多出了 客户端id和客户端密钥 生成的密钥
3.2、body参数
- grant_type :认证类型, 密码模式 填 password
- username : 用户名,user表里的username
- password :密码,user表里的密码
- scope : all
用户名和密码对应 user表里的 用户名和密码
数据库里存的密文, 页面上填明文。
最后调用该接口, 可以成功获取到token,证明 可以认证服务器 密码 模式 已经搭建成功。
4、资源服务器(下游服务)搭建
SpringCloud项目中的需要权限校验的下游服务 就是 对应 oauth2.0的模型中, 受保护的资源服务(Resource Server),需要进行权限校验后才能 正常访问。
我们把之前的micro-order服务改成 资源服务器,让他的接口需要权限校验。
之后 调用micro-order服务 就需要传有效的token才能访问。
4.1、pom依赖
新增oauth2.0权限校验 依赖
<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>
</dependency>
4.2、代码配置
4.2.1、客户端配置
/*
鉴权过滤器
* @ OAuth2AuthenticationProcessingFilter
*
* */
@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
// @Bean
public RequestInterceptor oauth2FeignRequestInterceptor(ClientCredentialsResourceDetails clientCredentialsResourceDetails) {
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails);
}
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
4.2.2、刷新token配置
public class RefreshTokenAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
@Autowired
private ClientCredentialsResourceDetails clientCredentialsResourceDetails;
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
@Autowired
RestTemplate restTemplate;
private static String oauth_server_url = "http://oauth-pwd-server/oauth/token";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
try {
//解析异常,如果是401则处理
ResponseEntity<?> result = exceptionTranslator.translate(authException);
if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add("client_id", clientCredentialsResourceDetails.getClientId());
formData.add("client_secret", clientCredentialsResourceDetails.getClientSecret());
formData.add("grant_type", clientCredentialsResourceDetails.getGrantType());
formData.add("scope", String.join(",", clientCredentialsResourceDetails.getScope()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
Map map = restTemplate.exchange(oauth_server_url, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
//如果刷新异常
if (map.get("error") != null) {
// 返回指定格式的错误信息
response.setStatus(401);
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.getWriter().print("{\"code\":1,\"message\":\"" + map.get("error_description") + "\"}");
response.getWriter().flush();
//如果是网页,跳转到登陆页面
//response.sendRedirect("login");
} else {
//如果刷新成功则存储cookie并且跳转到原来需要访问的页面
for (Object key : map.keySet()) {
response.addCookie(new Cookie(key.toString(), map.get(key).toString()));
}
request.getRequestDispatcher(request.getRequestURI()).forward(request, response);
// response.sendRedirect(request.getRequestURI());
//将access_token保存
}
} else {
//如果不是401异常,则以默认的方法继续处理其他异常
super.commence(request, response, authException);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2.3、资源服务器配置
@Configuration
@EnableResourceServer
//启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// @Autowired
// private TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// 配置order访问控制,必须认证后才可以访问
http.authorizeRequests()
.antMatchers("/order/**").authenticated();
}
/*
* 把token验证失败后,重新刷新token的类设置到 OAuth2AuthenticationProcessingFilter
* token验证过滤器中
* */
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
// resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint());
// resources.tokenStore(tokenStore);
}
}
4.3、配置文件
新增权限校验相关配置
# 配置 认证服务器 校验token的接口地址就行
security.oauth2.resource.user-info-uri=http://127.0.0.1:9052/security/check
security.oauth2.resource.prefer-token-info=false
4.4、测试下游服务 是否鉴权成功
利用 从认证服务器获取到的token,放到Authorization请求头里, 加前缀 bearer + 空格 +token,去直接请求 micro-order 的以下接口( 从网关路由到该服务的该接口也可以, 只要网关不过滤掉 Authorization 请求信息就行)。
@RequestMapping("/order")
@RestController
@RefreshScope
public class ConfigController {
@Value("${username}")
private String username;
// 方法级别控制,必须是ROLE_ADMIN 角色才能访问
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/getUsername")
public String getUsername(HttpServletRequest request) {
System.out.println(request.getHeader("Authorization"));
return username;
}
}
1、输入正确的token
发现是可以调用成功的
2、如果不输入token,报401未授权
3、输入错误的token,报401,token非法
至此, 资源服务器 确实 是 有权限校验的, 说明搭建成功。
5、认证服务器和下游系统权限校验流程
加上网关zuul, 大概的校验流程就是这样的:
zuul 携带 token 请求下游系统,被下游系统 filter 拦截
下游系统过滤器根据配置中的 user-info-uri 请求到认证服务器
请求到认证服务器被 filter 拦截进行 token 校验,把 token 对应的用户、和权限从数据库
查询出来封装到 Principal .
认证服务器 token 校验通过后过滤器放行执行 security/check 接口,把 principal 对象返回
下游系统接收到 principal 对象后就知道该 token 具备的权限了,就可以进行相应用户对
应的 token 的权限执行