OAuth2.0提供了四种授权模式
,refresh_token, 是否刷新token
授权码模式 authorization_code
密码模式 password
简化模式 implicit
客户端模式 client_credentials
此案例目前完成授权码模式,其他模式还没测试,后续会完善
学习笔记,直接上代码了
创建springboot项目,引入基本的依赖,mysql,mybatis,以及自动生成代码jar
springcloud版本 <spring-cloud.version>Greenwich.BUILD-SNAPSHOT</spring-cloud.version>
1. pom文件核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
2.首先看spring-security的配置类
package com.youdu.distributed.authentication.config;
import com.youdu.distributed.authentication.service.UserServiceDetail;
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.BeanIds;
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;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.servlet.http.HttpServletResponse;
/**
* @author Sir_小三
* @date 2020/1/30--13:01
* AuthorizationServer认证服务配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//使用密码模式,必须要配置)
@Autowired
private UserServiceDetail userServiceDetail;
/**
* //配置认证管理器(使用密码模式,必须要配置)
* 但是目前配置此bean,登陆,直接栈溢出error,还没解决
* @return
*/
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception{
return super.authenticationManagerBean();
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 所有请求都必须经过认证
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// .exceptionHandling()
// .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
// .and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin();//表单提交方式
//.successForwardUrl("/login/success");//登陆成功以后的controller
//.httpBasic(); 远程调用Basic方式,
}
/**
* 认证用户名密码
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
// auth.inMemoryAuthentication()//内存方式
// .passwordEncoder(passwordEncoder()) // 指定加密方式
// .withUser("lyj").password(passwordEncoder().encode("123456")).roles("ADMIN");
}
}
3.认证服务的配置类
package com.youdu.distributed.authentication.config;
import com.youdu.distributed.authentication.service.UserServiceDetail;
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.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.sql.DataSource;
import java.util.Arrays;
/**
* @author Sir_小三
* @date 2020/1/30--14:01
* AuthorizationServer认证服务配置
*/
@Configuration
@EnableAuthorizationServer//开启认证服务/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// @Autowired
// private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private DataSource dataSource;
// @Autowired
// private UserServiceDetail userServiceDetail;
/**
* 配置令牌访问端点得安全约束
* 必须设置allowFormAuthenticationForClients 否则没有办法用postman获取token
* 也需要指定密码加密方式BCryptPasswordEncoder
*
*/
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()") //公开获取token的url
.checkTokenAccess("permitAll()")//校验token的合法性
.allowFormAuthenticationForClients();
//.passwordEncoder(NoOpPasswordEncoder.getInstance());//不需要加密
}
/**
* //配置客户端详情,客户端详情可以通过数据库查询,(校验哪些客户端可以申请令牌)
* clientid 客户端id
* secret 客户端安全码
* scope 用来限制客户端得访问范围,默认为空,如果为空,那么客户端拥有全部得访问范围
* authorizedGrantTypes 此客户端可以使用得授权类型,默认为空,oauth2提供了五种授权类型
* authorities 此客户端可以使用 得权限 基于spring security authorities(一般不使用)
*
* @param clients
* @throws Exception
* 这里客户端信息存在数据库
*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());//配置客户端信息
//内存方式配置
// clients.inMemory().withClient("resources-service")//客户端id
// .secret(passwordEncoder.encode("123456"))//客户端密钥,需要加密,使用数据库存,数据库的要经过加密处理
// .resourceIds("res1")//资源id
// .authorizedGrantTypes("authorization_code","refresh_token","password")//oauth2 四种授权类型,目前两种,授权码,密码模式
// .scopes("all")
// .autoApprove(false)//授权码模式,false会跳转到授权页面,让用户授权,true不跳
// .redirectUris("http://www.baidu.com")//授权码模式,回调地址
// .accessTokenValiditySeconds(3600);
}
/**
* 配置令牌访问端点url,和令牌服务(令牌生成策略,如何生成)
* /oauth/authorize 授权端点
* 获取授权码
* http://localhost:8762/oauth/authorize?response_type=code&client_id=resources-service&scope=all&client_secret=123456&redirect_uri=http://www.baidu.com
* /oauth/token 获取令牌端点(发送post请求,获取token,code=申请的授权码)
*
* /oauth/confirm_access 用户确认授权端点
* /oauth/error 授权服务错误信息端点
* /oauth/check_token 用于资源服务访问的令牌解析端点
* /oauth/token_key 提供公有密钥的端点,如果使用jwt令牌的话
* 授权端点url应该被spring-security 保护起来,只供授权用户访问
* @param endpoints
* @throws Exception
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//.authenticationManager(authenticationManager)//认证管理服务,密码模式必须配置,还有userServiceDetail
.authorizationCodeServices(authorizationCodeServices)//授权码模式
.tokenStore(tokenStore)
//在这里设置jwtAccessTokenConverter,发现是ok的,可以生成jwt令牌
.accessTokenConverter(jwtAccessTokenConverter)
//.userDetailsService(userServiceDetail)//密码模式要配
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService());//客户端详情服务`ervice.setSupportRefreshToken(true);//支持刷新令牌-*
service.setTokenStore(tokenStore);//令牌存储策略
//令牌增强,使用jwt令牌
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
//在这里设置jwtAccessTokenConverter,但是发现不起作用,令牌还是普通令牌,加到发现可以
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setSupportRefreshToken(true); //支持刷新令牌
service.setAccessTokenValiditySeconds(7200);//令牌默认有效两小时
service.setRefreshTokenValiditySeconds(259200);//刷新令牌默认有效期三天
return service;
}
//设置授权码模式的授权码如何存取
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
return new JdbcAuthorizationCodeServices(dataSource);
}
//内存方式存储授权码
// @Bean
// public AuthorizationCodeServices authorizationCodeServices(){
// return new InMemoryAuthorizationCodeServices();
// }
/**
* 客户端配置从数据库读取,发起授权时,会查询数据库数据进行认证,可添加n条客户端数据,获取验证码
* 客户端拿到验证码,请求/oauth/token,获取令牌,
* 注意!!!数据库中存储客户端密钥,需要使用BCryptPasswordEncoder加密后,在存,
* 不然获取token的时候会报错(说不是BCryptPasswordEncoder格式)
* (应该是将你请求的密钥123456经过加密,和数据库中的数据做比对,ok,则发送令牌)
* 获取token需要的参数
* "client_id":"r1",
* "client_secret":"123456",
* "grant_type":"authorization_code",
* "scope":"all",
* "redirect_uri":"http://www.baidu.com",
* "code":"5AsJkQ",
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
}
4.JwtTokenConfig 配置类,暂时使用对称加密
package com.youdu.distributed.authentication.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* @author Sir_小三
* @date 2020/1/30--14:34
*/
@Configuration
public class TokenConfig {
//对称加密,密钥,也可采用非对称加密
private String SIGNING_KRY="lyj123";
/**
* 使用密钥生成令牌
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KRY);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
//内存存储简单令牌
// @Bean
// public TokenStore tokenStore() {
// return new InMemoryTokenStore();
// }
}
5.还有一个重要的,UserServiceDetail 配置
这里SecurityUser 去继承security.core.userdetails.User提供的user
此类来验证登陆用户密码
package com.youdu.distributed.authentication.service;
import com.youdu.distributed.authentication.entity.SecurityUser;
import com.youdu.distributed.authentication.entity.TbUser;
import com.youdu.distributed.authentication.mapper.TbUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author Sir_小三
* @date 2020/1/30--12:54
*/
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private TbUserMapper tbUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
TbUser tbUser = new TbUser();
tbUser.setName(username);
List<TbUser> page = tbUserMapper.page(tbUser, null);
TbUser tbUser1 = page.get(0);
SecurityUser securityUser = new SecurityUser(username, tbUser1.getPassword(),grantedAuthorities);
return securityUser;
}
}
package com.youdu.distributed.authentication.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* @author Sir_小三
* @date 2020/1/30--18:56
*/
public class SecurityUser extends User {
public SecurityUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public SecurityUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
6.到这里重要代码基本就贴完了,剩下的就没什么了,贴一下配置文件,主要就eureka地址,以及mysql连接
#client每30秒会发送心跳给eureka进行续约,如果eureka90秒没有收到客户端心跳,那么注册中心会剔除此client
#client注册到eureka时,会将自己的ip地址等信息进行提供,eureka会存所有客户端的注册信息表,client会将其获取到本地内存,进行调用
server.port:8762
#需要指明spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个name 。
spring.application.name=distributed-authentication
#指定erreka-server注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
#开启feign断路器熔断机制,Feign是自带断路器的,在D版本的Spring Cloud之后,它没有默认打开。需要在配置文件中配置打开它
#feign.hystrix.enabled=true
#指定链路跟踪地址zipkin server
#spring.zipkin.base-url=http://localhost:9411
#调用的超时时间 springcloud默认情况下一秒之内调用成功,否则将打开断路器
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.url=jdbc:mysql://localhost:3306/rootdatabase?useUnicode=true&characterEncoding=utf-8&useSSL=false
#spring.datasource.username=root
#spring.datasource.password=123456
mybatis.mapper-locations=classpath:mapper/*.xml
#tx-lcn.client.manager-address=127.0.0.1:8070 tc默认连接tm的ip端口
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=PRC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.main.allow-bean-definition-overriding=true
7.提供一下oauth2的数据库表,这都是由oauth2提供的,直接运行生成表即可
CREATE SCHEMA IF NOT EXISTS `oauth2` DEFAULT CHARACTER SET utf8 ;
USE `oauth2` ;
-- -----------------------------------------------------
-- Table `oauth2`.`clientdetails`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`clientdetails` (
`appId` VARCHAR(128) NOT NULL,
`resourceIds` VARCHAR(256) NULL DEFAULT NULL,
`appSecret` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`grantTypes` VARCHAR(256) NULL DEFAULT NULL,
`redirectUrl` VARCHAR(256) NULL DEFAULT NULL,
`authorities` VARCHAR(256) NULL DEFAULT NULL,
`access_token_validity` INT(11) NULL DEFAULT NULL,
`refresh_token_validity` INT(11) NULL DEFAULT NULL,
`additionalInformation` VARCHAR(4096) NULL DEFAULT NULL,
`autoApproveScopes` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`appId`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_access_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_access_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL,
`refresh_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_approvals`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_approvals` (
`userId` VARCHAR(256) NULL DEFAULT NULL,
`clientId` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`status` VARCHAR(10) NULL DEFAULT NULL,
`expiresAt` DATETIME NULL DEFAULT NULL,
`lastModifiedAt` DATETIME NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_client_details`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_client_details` (
`client_id` VARCHAR(128) NOT NULL,
`resource_ids` VARCHAR(256) NULL DEFAULT NULL,
`client_secret` VARCHAR(256) NULL DEFAULT NULL,
`scope` VARCHAR(256) NULL DEFAULT NULL,
`authorized_grant_types` VARCHAR(256) NULL DEFAULT NULL,
`web_server_redirect_uri` VARCHAR(256) NULL DEFAULT NULL,
`authorities` VARCHAR(256) NULL DEFAULT NULL,
`access_token_validity` INT(11) NULL DEFAULT NULL,
`refresh_token_validity` INT(11) NULL DEFAULT NULL,
`additional_information` VARCHAR(4096) NULL DEFAULT NULL,
`autoapprove` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`client_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_client_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_client_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_code`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_code` (
`code` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_refresh_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_refresh_token` (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
8.获取授权码url参考
首先发送这个请求,参数要换成自己的,由于spring-security的配置,所有请求必须认证通过,所以会跳至spring-security提供的登陆进行认证,登陆成功,还需认证客户端参数,是否于数据库配置一样,一样,则返回授权页面,进行授权,用户点击授权,跳转到指定url,并发放授权码,
拿到授权码,认证服务器会将授权码存在数据库,一个授权码只能申请一次令牌
接下来,使用授权码获取jwt令牌
http://localhost:8762/oauth/authorize?response_type=code&client_id=resources-service&scope=all&client_secret=123456&redirect_uri=http://www.baidu.com
获取到code图片
9.获取到授权码,postman通过获取的授权码,申请jwt令牌
10.校验令牌
通过令牌访问资源服务器
11.以后客户端拿令牌,访问资源服务,目前资源服务还没做好,做好后期会更新,还要密码授权模式,也没搞清除,感觉密码模式加jwt可以做分布式的单点登陆,后续有时间会更新的,有不清除的或者想要源码的