oauth2.0也是开发中用的比较多的一种授权机制,它主要用来颁发令牌;OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。......资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。
通过上面对oauth的介绍,我们已经大概知道它的用途了,现在由于各种原因oauth1几乎已经没人再用了,目前用的较多的是oauth2.0; oauth2.0规定了四种获得令牌的流程,如下所示:
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password):
- 客户端凭证(client credentials)
对于后端开发来说授权码的模式是使用较多的也相对安全一些,本次 分享的也就是该模式了
1 开发前的准备
由于我们使用的是spring security oauth2.0,鉴于此官方也为我们提供了oauth2.0开发相关参考如用到的表结构、整合代码等,下面是官方提供的表结构
数据表的字段释义我也找了相关的资料可以参考下面这篇文章:
https://blog.csdn.net/wangxuelei036/article/details/109491215
但是这些sql直接在mysql里执行是要报错的,也是一个小小的坑,报错的地方就是“token LONGVARBINARY“里定义的这个longvarbinary了,原因是mysql并不支持这种数据类型,解决的方式很简单就是把它全部改成blob数据类型就可以了
稍作修改之后,执行这些sql生成表到自己的数据库里就ok了,此外我们只需要在项目引入maven的依赖准备工作就完成了
<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>
如果项目已经有security的依赖那么只需要引入oauth2的就可以了
2 oauth服务端的开发
(1) 服务端也需要使用spring security,毫无疑问我们需要先配置spring security
package com.debug.security;
import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityUserService securityUserService;
@Bean
public BCryptPasswordEncoder myPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()//指定认证页面可以匿名访问 //关闭跨站请求防护
.and().csrf().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception { //UserDetailsService类
auth.userDetailsService(securityUserService)
.passwordEncoder(myPasswordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
这里的spring security配置就比较简单了,只是指定了密码加密、认证url等,认证登录也可以定义成自己的,这里方便起见就直接用spring security默认的
(2) 认证授权的代码逻辑,客观来说此处只用到认证,代码如下:
package com.debug.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.debug.entity.TSystemPermission;
import com.debug.entity.TSystemRole;
import com.debug.entity.TSystemUser;
import com.debug.security.MyAuthenticationProvider;
import com.debug.service.SecurityUserService;
import com.debug.service.TSystemPermissionService;
import com.debug.service.TSystemRoleService;
import com.debug.service.TSystemUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SecurityUserServiceImpl implements SecurityUserService {
@Autowired
private TSystemUserService tSystemUserService;
@Autowired
private TSystemRoleService tSystemRoleService;
@Autowired
private TSystemPermissionService tSystemPermissionService;
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<TSystemUser> qw = new QueryWrapper<TSystemUser>();
qw.eq("login_name", username);
TSystemUser user = tSystemUserService.getOne(qw);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List<TSystemPermission> permissionList = tSystemPermissionService.getRolePermission(user.getId(), "1");
List<TSystemRole> roleList = tSystemRoleService.getUserRole(user.getId());
String roleName = roleList.get(0).getName();
StringBuffer buf = new StringBuffer();
for (TSystemPermission permission : permissionList) {
String per = permission.getPermission();
buf.append(per + ",");
}
String sp = buf.toString().substring(0, buf.toString().lastIndexOf(","));
List<GrantedAuthority> authList = AuthorityUtils.commaSeparatedStringToAuthorityList(roleName + "," + sp);
UserDetails u = new User(user.getLoginName(), user.getPassword(), authList);
return u;
}
public String login(String username, String password) {
QueryWrapper<TSystemUser> qw = new QueryWrapper<TSystemUser>();
qw.eq("login_name", username);
TSystemUser user = tSystemUserService.getOne(qw);
// 这里我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
if (!new BCryptPasswordEncoder().matches(password, user.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
//return jwtTokenUtil.generateToken(username);
return null;
}
public String refreshToken(String oldToken) {
String token = oldToken;
return "error";
}
}
认证的代码和昨天写的是一样的,几乎没有任何修改
(3) oauth服务端的配置
这一步的配置相对麻烦一点,我们需要配置Oauth的数据源(和之前新建的oauth开头的那几张表有关)、token保存策略、授权信息保存策略、客户端登录信息来源等
package com.debug.config;
import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityUserService securityUserService;
//从数据库中查询出客户端信息
@Bean
public JdbcClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
//token保存策略
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
//授权信息保存策略
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
//授权码模式专用对象
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
//指定客户端登录信息来源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.userDetailsService(securityUserService)
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
}
3 oauth资源端的开发
资源端的开发除了加入资源端相关配置外,其他地方和普通的spring boot项目无异,先来看一下资源端的配置
package com.debug.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
/*** TokenStore是OAuth2保存token的接口 * 其下有RedisTokenStore保存到redis中, * JdbcTokenStore保存到数据库中, * InMemoryTokenStore保存到内存中等实现类, * 这里我们选择保存在数据库中 * @return */
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
TokenStore tokenStore = new JdbcTokenStore(dataSource);
resources.resourceId("product_api") //指定当前资源的id,非常重要!必须写!
.tokenStore(tokenStore); //指定保存token的方式
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
.and().headers().addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请 求头信息
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access- Control-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access- Control-Request-Headers"));
}
});
}
}
最需要注意的就是这个 资源端ID了 ,这个ID除了在代码中配置也需要添加到数据库中,如下所示,在oauth_client_details表里加入一条记录,sql如下:
INSERT INTO `oauth_client_details` VALUES ('debugxwz', 'product_api', '$2a$10$WZQaLHfS6amrJzN50wE3e.upn8KIi1wmCH9FSdZE6OBt8OKSyGLm.', 'read, write', 'client_credentials,implicit,authorization_code,refresh_token,password', 'http://www.baidu.com', NULL, NULL, NULL, NULL, 'false');
client_id和client_secret在后面的获取token接口中需要使用到所以不能写错,密码的生成也是使用springsecurity的那一套,这里不做过多解释
接下来编写一个测试用的controller,代码如下:
package com.debug.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping
public String findAll() {
return "查询产品列表成功!";
}
}
4 测试
先在浏览器敲 http://localhost:8082/oauth/authorize?response_type=code&client_id=debugxwz
这一步的作用是获取code,在获取code之前需要经过spring security,根据配置会使用spring security提供的登录页面,输入我们的账号和密码(不是oauth_client_details的)
登录后浏览器跳转到一个询问页面,就类似微信授权那个
点击下面的授权按钮则跳转到我们制定的url并携带code , 此处为了方便数据库跳转链接直接写的百度,日常开发就换成自己的并保证外网可以访问到就行
到此为止我们就取得code了
接下来我们使用postman来获取token,url地址为 http://localhost:8082/oauth/token
需要post提交的参数有四个分别是grant_type(此处填写authorization_code)、client_id、client_secret、code 如下所示:
到此为止我们就获取到access_token了,接下来就可以拿这个access_token去访问资源了(刚才的ProductController),地址如下:
http://localhost:8081/product?access_token=e0ac892e-bbee-4ba6-b348-88d43e66de8a
到此为止一个简单oauth2.0服务端和资源端的代码就开发完成了
其他参考资料:http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html