先说OAuth,OAuth是Open Authorization的简写。OAuth协议为用户资源的授权提供一个安全的,开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的账号信息,即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
授权码
(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
https://b.com/oauth/authorize?response_type=code&client_id=CLIENT_ID
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户选择同意,则B 网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码。
https://a.com/?code=AUTHORIZATION_CODE
上面 URL 中,code
参数就是授权码。
http://b.com/oauth/token?grant_type=authorization_code&code=O6Wn5w&client_secret=SECRET&client_id=CLIENT_ID
上面 URL 中,grant_type
参数表示采用授权码模式获取token令牌,code
参数就是授权码,client_sercret
参数是用户的密码,client_id
参数指谁在请求。
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段 JSON 数据。
{
"access_token": "088dd9b2-a49d-457b-a1cc-4bb9f0e932f6",
"token_type": "bearer",
"refresh_token": "beb688c6-8b02-40fc-be74-fc1a3e4c07aa",
"expires_in": 43199,
"scope": "read write"
}
上面 JSON 数据中,access_token
字段就是令牌,A 网站在后端拿到了
(1)首先需要了解OAuth2.0中的表结构说明
因为案例中主要用到的表是oauth_client_details
,在这里主要介绍这张核心表,其他的表的功能可以到该网站查看
https://andaily.com/spring-oauth-server/db_table_description.html
oauth_client_details
表说明
字段名 | 字段说明 |
---|---|
client_id | 主键,必须唯一,用于唯一标识每一个客户端 |
resource_ids | 客户端所能访问的资源id集合 |
client_secret | 用于指定客户端的访问密匙 |
scope | 指定客户端申请的权限范围,可选值包括read,write,trust |
authorized_grant_types | 指定客户端支持的grant_type,可选值包括 authorization_code,password,refresh_token,implicit,client_credentials |
web_server_redirect_uri | 客户端的重定向URI |
authorities | 指定客户端所拥有的Spring Security的权限值 |
access_token_validity | 设定客户端的access_token的有效时间值 |
additional_information | 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的 数据 |
archived | 用于标识客户端是否已存档(即实现逻辑删除),默认值为’0’(即未存档) |
trusted | 设置客户端是否为受信任的,默认为’0’ |
autoapprove | 设置用户是否自动Approval操作, 默认值为 ‘false’ |
(2)建表操作
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
除此之外,还需要创建用户表,角色表,用户角色关系表来实现用数据库信息认证
用户表
(sys_user)
id | username | password |
---|---|---|
1 | admin | $2a 10 10 10xQjca8d71Pkx2xPmasouQemol15C5KNn814B0cnok2o8FNK7lQCia |
2 | user | $2a 10 10 10xQjca8d71Pkx2xPmasouQemol15C5KNn814B0cnok2o8FNK7lQCia |
要注意的地方,密码是以BCryptPasswordEncoder加密保存的,我这里的明文是123,但是生成加密密文是随机的,所以在设置密码的时候,需要自己去动手生成属于自己的加密密文
System.out.println(new BCryptPasswordEncoder().encode("passsword"));
角色表
(sys_role)
ID | ROLE_NAME | ROLE_DESC |
---|---|---|
1 | ROLE_USER | 基本角色 |
2 | ROLE_ADMIN | 超级管理员 |
用户角色关系表
(sys_user_role)
user_id | role_id |
---|---|
1 | 1 |
1 | 2 |
2 | 1 |
(3)创建maven项目
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
<version>2.2.0.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: 123456
main:
allow-bean-definition-overriding: true
logging:
level:
com.itheima: debug
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.yanc.domain
OauthServerConfig
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
//数据库连接池对象
@Autowired
private DataSource dataSource;
//认证业务对象
@Autowired
private AuthenticationManager authenticationManager;
//授权模式专用对象
@Autowired
private UserService userService;
//从数据库中查询出客户端信息
@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());
}
//检查token的策略
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("isAuthenticated()");
}
//OAuth2的配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
}
WebSecurityConfig
@Configuration
:设置为配置类
@EnableWebSecurity
:AOP拦截器
@EnableGlobalMethodSecurity
(prePostEnabled
= true
)
//AOP,拦截器
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
//设置权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
//注入PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//指定认证对象的来源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//AuthenticationManager对象在Oauth2认证服务中要使用,提取放到IOC容器中
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
OauthSourceConfig
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
/**
* 指定token的持久化策略
* InMemoryTokenStore表示将token存储在内存中
* Redis表示将token存储在redis中
* JdbcTokenStore表示将token存储在数据库中
* @return
*/
@Bean
public TokenStore jdbcTokenStore(){
return new JdbcTokenStore(dataSource);
}
/**
* 指定当前的资源id和存储方案
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("product_api").tokenStore(jdbcTokenStore());
}
@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("AccessControl-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("AccessControl-Request-Headers"));
}
});
}
}
SysUser.java
(此处使用了 Lombok 简化代码)@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
@TableField(exist = false)
private List<SysRole> roles;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
SysRole.java
@Data
public class SysRole implements GrantedAuthority{
private Integer id;
private String roleName;
private String roleDesc;
@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
}
public interface UserMapper {
@Select("select * from sys_user where username = #{username}")
@Results({
@Result(id = true,property = "id", column = "id"),
@Result(property = "roles",column = "id",javaType = List.class,
many = @Many(select = "com.yanc.mapper.RoleMapper.findByUid"))
})
SysUser findByName(String username);
}
public interface RoleMapper {
@Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc "+
"FROM sys_role r ,sys_user_role ur "+
"WHERE r.id=ur.rid AND ur.uid=#{uid}")
public List<SysRole> findByUid(Integer uid);
}
public interface UserService extends UserDetailsService {
}
@Service
@Transactional//添加事务管理
public class UserServiceImpl implements UserService {
@Autowired(required = false)
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userMapper.findByName(s);
}
}
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/findAll")
public String findAll(){
return "授权成功";
}
}
http://localhost:9001/oauth/authorize?response_type=code&client_id=yanc
response_type参数表示要求返回授权码,client_id指定当前请求的用户
4. 我们拿到授权码后,就可以在postman申请token了
点击send后,成功会返回一段json数据,access_token就是我们所需的令牌