結(jié)構(gòu)基礎(chǔ)
基礎(chǔ)框架:Spring Boot + Spring-Security-OAuth2
存儲(chǔ)介質(zhì):Mysql + Redis
持久化方式:Spring-data-jpa
測(cè)試工具:Postman
大局觀:
1黎棠、OAuth2服務(wù)器分為兩部分組成:認(rèn)證授權(quán)服務(wù)器和資源服務(wù)器晋渺。聞名知意,不解釋脓斩。本文只講認(rèn)證授權(quán)服務(wù)器的搭建木西,資源服務(wù)器部分后續(xù)。
2随静、認(rèn)證授權(quán)服務(wù)器分為兩大步驟八千,一是認(rèn)證,二是授權(quán)燎猛。而認(rèn)證則主要由Spring-Security負(fù)責(zé)恋捆,而授權(quán)則有Oauth2負(fù)責(zé)。
3重绷、本項(xiàng)目有2個(gè)存儲(chǔ)介質(zhì)沸停,Mysql和Redis。Mysql的作用是用來(lái)存儲(chǔ)認(rèn)證數(shù)據(jù)昭卓,而Redis用作緩存和存儲(chǔ)授權(quán)信息及AccessToken的愤钾。其實(shí),Mysql同事可以用來(lái)存儲(chǔ)認(rèn)證數(shù)據(jù)和存儲(chǔ)授權(quán)信息以及AccessToken的候醒,而且Spring-Security-OAuth2也提供了存儲(chǔ)基礎(chǔ)能颁。那么問(wèn)題來(lái)了,為什么不用Mysql呢倒淫?考慮原因:AccessToken是有時(shí)效性的伙菊,也就是說(shuō),存儲(chǔ)一段時(shí)間后敌土,將會(huì)失效镜硕,也許是一天或者一個(gè)月。在單體應(yīng)用情況下纯赎,當(dāng)業(yè)務(wù)比較多谦疾、訪問(wèn)頻率大的時(shí)候,如果使用mysql犬金,那么有可能導(dǎo)致響應(yīng)速度降低念恍,基于性能的考慮,減小數(shù)據(jù)庫(kù)的壓力晚顷,所以將其改良為使用Redis存儲(chǔ)授權(quán)信息和AccessToken峰伙。而Redis性能十分優(yōu)越,同時(shí)還能作為緩存認(rèn)證信息使用该默,一舉兩得瞳氓,何樂(lè)而不為呢?
學(xué)習(xí)基礎(chǔ)
參考理解OAuth 2.0 - 阮一峰的網(wǎng)絡(luò)日志
認(rèn)證方式
Oauth2授權(quán)有多種方式栓袖,此處將使用grant_type為client_secret和password兩種方式匣摘。
1店诗、客戶端授權(quán)(Client Credentials Grant)
POST /oauth2-server/oauth/token?grant_type=client_credentials HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
請(qǐng)求信息如上。注意事項(xiàng)如下:
1音榜、在mysql中建立基礎(chǔ)表:oauth_client_details庞瘸,查看建表以及初始化。其中client_id=client_auth_mode,client_secret的原始值為123456赠叼,數(shù)據(jù)庫(kù)中存儲(chǔ)的是加密后的值擦囊,加密方式為BCrypt。
2嘴办、請(qǐng)求頭:key=Authorization瞬场;value=Basic+空格+Base64(username:password)
3、Basic后面的信息由[username:password]內(nèi)的字符Base64加密而成
4、此中的username和password分別為oauth_client_details表中的client_id和client_secret,也就是客戶端模式下的標(biāo)識(shí)客戶端的憑證(用以區(qū)別是哪種受信任的客戶端),對(duì)應(yīng)OAuth2映射為ClientDetails對(duì)象。
2冶忱、密碼授權(quán)
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
請(qǐng)求信息如上。注意事項(xiàng)如下:
1迟蜜、在mysql中建立基礎(chǔ)表:oauth_client_details和ux_member向图,查看建表以及初始化。其中oauth_client_details表中client_id=password_auth_mode,client_secret的原始值為123456桌吃,數(shù)據(jù)庫(kù)中存儲(chǔ)的是加密后的值朱沃,加密方式為BCrypt。ux_member表中茅诱,username=member_name,password=123456逗物,加密方式MD5。
1瑟俭、請(qǐng)求頭:key=Authorization翎卓;value=Basic+空格+Base64(username:password)
2、Basic后面的信息由[username:password]內(nèi)的字符Base64加密而成
3摆寄、此中的username和password依舊為oauth_client_details表中的client_id和client_secret失暴,也就是客戶端模式下的標(biāo)識(shí)客戶端的憑證(用以區(qū)別是哪種受信任的客戶端),對(duì)應(yīng)OAuth2映射為DetailDetails對(duì)象微饥。
4逗扒、由上至少可看出二者在傳參時(shí)的表面上的區(qū)別,只是密碼授權(quán)模式欠橘,多了2個(gè)參數(shù):username和password矩肩,以及grant_type的值不一樣。而里層的區(qū)別肃续,在于密碼模式下黍檩,Spring-Security-Oauth2中叉袍,有個(gè)叫做UserDetails的對(duì)象,而剛好ux_member表就是與之對(duì)應(yīng)刽酱。
大局觀已有喳逛,廢話少說(shuō),下面開始講述相關(guān)配置
存儲(chǔ)介質(zhì)
- Mysql
a肛跌、作用:存儲(chǔ)認(rèn)證管理信息和業(yè)務(wù)數(shù)據(jù)艺配。那么問(wèn)題來(lái)了,什么稱之為認(rèn)證信息呢衍慎?我的理解為能標(biāo)識(shí)用戶主體是誰(shuí)的唯一性的信息转唉,這里的主體可能為客戶端也可能為某個(gè)PC或者移動(dòng)端的某個(gè)人。
b稳捆、設(shè)計(jì):在本項(xiàng)目中赠法,所謂的認(rèn)證信息有2個(gè),oauth_client_details與ux_member表乔夯。與之對(duì)應(yīng)的也就是ClientDetails和UserDetails對(duì)象砖织。這兩個(gè)都是待認(rèn)證的主體,也就是說(shuō)在客戶端模式下末荐,需要對(duì)ClientDetails對(duì)象進(jìn)行認(rèn)證侧纯;而在密碼模式下,則既需要對(duì)ClientDetails對(duì)象認(rèn)證甲脏,也需要對(duì)UserDetails對(duì)象認(rèn)證眶熬。
- Redis
a、存儲(chǔ)授權(quán)信息以及AccessToken
b块请、緩存密碼模式下的認(rèn)證信息(UserDetails對(duì)象娜氏,以u(píng)sername為key)
配置信息
security:
basic:
enabled: false # 是否開啟基本的鑒權(quán),默認(rèn)為true墩新。 true:所有的接口默認(rèn)都需要被驗(yàn)證贸弥,將導(dǎo)致 攔截器[對(duì)于 excludePathPatterns()方法失效]
server:
context-path: /oauth2-server
port: 8050
---
spring:
application:
name: oauth2-server
redis:
database: 4
host: 127.0.0.1
password: root123456
port: 6379
pool:
max-active: 8
max-wait: 8
min-idle: 0
max-idle: 8
datasource:
# dataSourceClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/redis-oauth2?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
database: MYSQL
openInView: true
show_sql: true
generate-ddl: true #(false)
hibernate:
ddl-auto: update #(none)
在resources文件夾下建立一個(gè)application.yml文件,然后把上述信息拷貝進(jìn)去海渊,即可绵疲。
因?yàn)楸卷?xiàng)目是基于Spring Boot的開發(fā),Spring Boot其中一個(gè)好處就是能夠根據(jù)你的配置信息自動(dòng)生成相關(guān)的Bean對(duì)象切省,如數(shù)據(jù)源DataSource最岗、緩存工廠類RedisConnectionFactory、緩存RedisCache等Bean對(duì)象朝捆。
數(shù)據(jù)存儲(chǔ)配置
@Configuration
public class DataStoreConfig {
public static final String REDIS_CACHE_NAME="redis_cache_name";//不為null即可
public static final String REDIS_PREFIX ="redis_cache_prefix";//不為null即可
public static final Long EXPIRE =60*60L;//緩存有效時(shí)間
/**
* 配置用以存儲(chǔ)用戶認(rèn)證信息的緩存
*/
@Bean
RedisCache redisCache(RedisTemplate redisTemplate){
RedisCache redisCache = new RedisCache(REDIS_CACHE_NAME,REDIS_PREFIX.getBytes(),redisTemplate,EXPIRE);
return redisCache;
}
/**
*
* 創(chuàng)建UserDetails存儲(chǔ)服務(wù)的Bean:使用Redis作為緩存介質(zhì)
* UserDetails user = this.userCache.getUserFromCache(username)
*/
@Bean
public UserCache userCache(RedisCache redisCache) throws Exception {
UserCache userCache = new SpringCacheBasedUserCache(redisCache);
return userCache;
}
/**
* 配置AccessToken的存儲(chǔ)方式:此處使用Redis存儲(chǔ)
* Token的可選存儲(chǔ)方式
* 1、InMemoryTokenStore
* 2、JdbcTokenStore
* 3驯用、JwtTokenStore
* 4脸秽、RedisTokenStore
* 5、JwkTokenStore
*/
@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
return new RedisTokenStore(redisConnectionFactory);
}
}
Domain層簡(jiǎn)述
@Entity
@Table(name = "ux_member")
public class Member implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
public Member(Member member){
super();
this.username = member.getUsername();
this.password = member.getPassword();
}
public Member() {
}
//略過(guò)getter和setter
}
//默認(rèn)角色
public class Role implements GrantedAuthority {
private static final long serialVersionUID = -2633659220734280260L;
private Set<Role> roles = new HashSet<Role>();
@Override
public String getAuthority() {
return "USER";
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
Dao層
@Component("memberRepository")
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findOneByUsername(String username);
}
Service層
@Service
public class CustomUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findOneByUsername(username);
if (member == null) {
log.error("用戶不存在");
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(member);
}
/**
* 注意該類的層次結(jié)構(gòu)蝴乔,繼承了Member并實(shí)現(xiàn)了UserDetails接口记餐,繼承是為了使用Member的username和password信息
*/
private final static class UserRepositoryUserDetails extends Member implements UserDetails {
private static final long serialVersionUID = 1L;
private UserRepositoryUserDetails(Member member) {
super(member);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Role role = new Role();
return role.getRoles();
}
@Override
public String getUsername() {
return super.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
自定義認(rèn)證服務(wù)器類:用來(lái)對(duì)UserDetails信息進(jìn)行認(rèn)證,CustomUserDetailsService類實(shí)現(xiàn)了UserDetailsService接口薇正,而UserDetailsService則是用來(lái)對(duì)UserDetails進(jìn)行認(rèn)證檢查的片酝,該項(xiàng)目是基于SpringBoot的,所以挖腰,該Bean對(duì)象將會(huì)注入依賴該Bean的其他的Bean對(duì)象中雕沿,如DaoAuthenticationProvider、DefaultTokenServices等猴仑,并在相關(guān)的認(rèn)證流程中對(duì)UserDetails進(jìn)行檢查审轮。
認(rèn)證授權(quán)配置
1、Spring-Security-OAuth2對(duì)于認(rèn)證信息的存儲(chǔ)提供了如下方案:數(shù)據(jù)庫(kù)和內(nèi)存辽俗。而此處將使用Mysql存儲(chǔ)疾渣。
2、認(rèn)證管理信息的配置主要是針對(duì)ClientDetails和UserDetails對(duì)象的檢查崖飘,客戶端模式針對(duì)ClientDetails檢查榴捡,而密碼模式則先檢查ClientDetails后檢查UserDetails對(duì)象。
認(rèn)證授權(quán)配置如下
@Configuration
@EnableAuthorizationServer//開啟配置 OAuth 2.0 認(rèn)證授權(quán)服務(wù)
public class AuthAuthorizeConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* 配置 oauth_client_details【client_id和client_secret等】信息的認(rèn)證【檢查ClientDetails的合法性】服務(wù)
* 設(shè)置 認(rèn)證信息的來(lái)源:數(shù)據(jù)庫(kù) (可選項(xiàng):數(shù)據(jù)庫(kù)和內(nèi)存,使用內(nèi)存一般用來(lái)作測(cè)試)
* 自動(dòng)注入:ClientDetailsService的實(shí)現(xiàn)類 JdbcClientDetailsService (檢查 ClientDetails 對(duì)象)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
/**
* 密碼模式下配置認(rèn)證管理器 AuthenticationManager,并且設(shè)置 AccessToken的存儲(chǔ)介質(zhì)tokenStore,如果不設(shè)置朱浴,則會(huì)默認(rèn)使用內(nèi)存當(dāng)做存儲(chǔ)介質(zhì)薄疚。
* 而該AuthenticationManager將會(huì)注入 2個(gè)Bean對(duì)象用以檢查(認(rèn)證)
* 1、ClientDetailsService的實(shí)現(xiàn)類 JdbcClientDetailsService (檢查 ClientDetails 對(duì)象)
* 2赊琳、UserDetailsService的實(shí)現(xiàn)類 CustomUserDetailsService (檢查 UserDetails 對(duì)象)
*
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore).userDetailsService(userDetailsService);
}
/**
* 配置:安全檢查流程
* 默認(rèn)過(guò)濾器:BasicAuthenticationFilter
* 1、oauth_client_details表中clientSecret字段加密【ClientDetails屬性secret】
* 2砰碴、CheckEndpoint類的接口 oauth/check_token 無(wú)需經(jīng)過(guò)過(guò)濾器過(guò)濾躏筏,默認(rèn)值:denyAll()
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();//允許客戶表單認(rèn)證
security.passwordEncoder(new BCryptPasswordEncoder());//設(shè)置oauth_client_details中的密碼編碼器
security.checkTokenAccess("permitAll()");//對(duì)于CheckEndpoint控制器[框架自帶的校驗(yàn)]的/oauth/check端點(diǎn)允許所有客戶端發(fā)送器請(qǐng)求而不會(huì)被Spring-security攔截
}
}
啟動(dòng)服務(wù)器
@SpringBootApplication
public class Oauth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2ServerApplication.class, args);
}
}
Postman測(cè)試
客戶端授權(quán)模式獲取AccessToken請(qǐng)求如下:
請(qǐng)求的報(bào)文信息如下:
POST /oauth2-server/oauth/token?grant_type=client_credentials HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
Cache-Control: no-cache
Postman-Token: e5d3ea12-af31-d344-8804-f92db46112a3
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
返回結(jié)果如:
{
"access_token": "afef641c-62de-4f5d-a5b8-7864ac2b7127",
"token_type": "bearer",
"expires_in": 3463,
"scope": "read write"
}
密碼授權(quán)模式獲取AccessToken請(qǐng)求如下:
請(qǐng)求的報(bào)文信息如下:
POST /oauth2-server/oauth/token?username=member_name&password=e10adc3949ba59abbe56e057f20f883e&grant_type=password&client_id=password_auth_mode&client_secret=123456 HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic cGFzc3dvcmRfYXV0aF9tb2RlOjEyMzQ1Ng==
Cache-Control: no-cache
Postman-Token: 0ccf7ea9-c2ac-10bc-a9da-3d15de82840b
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
返回結(jié)果如:
{
"access_token": "a83ba33f-9f1a-4f9a-ba65-99e7fc905ba2",
"token_type": "bearer",
"refresh_token": "89f724d6-8553-4838-b4ff-7f6c8fb4d88b",
"expires_in": 3378,
"scope": "read write"
}
結(jié)果對(duì)比
差異:客戶端授權(quán)返回結(jié)果比密碼模式返回結(jié)果少了一個(gè)refresh_token,因?yàn)榭蛻裟J讲恢С謗efresh_token認(rèn)證呈枉。
原因:client_credentials是受信任的認(rèn)證模式趁尼,也就意味著你對(duì)于此種信息都是信任的,即可以設(shè)置為永久性的AccessToken猖辫,而不需要刷新重新獲取AccessToken酥泞。
總結(jié)
對(duì)于Spring-Security-Oauth2的學(xué)習(xí)和研究,陸陸續(xù)續(xù)地持續(xù)了不少時(shí)間啃憎,零零散散地也做了不少的筆記芝囤,踩了不少的坑,不奇怪,Spring-Security-OAuth2都沒(méi)個(gè)官方文檔悯姊。寫文章的時(shí)候羡藐,也是一邊敲著代碼,一邊優(yōu)化著悯许,去除了不少無(wú)用的代碼仆嗦,也理清了頭緒。如有錯(cuò)誤先壕,還請(qǐng)大牛們指出瘩扼。
源代碼地址:oauth2-redis-mysql[提醒,直接導(dǎo)入我的項(xiàng)目前垃僚,需要啟動(dòng)redis服務(wù)集绰,并修改相關(guān)的redis配置和數(shù)據(jù)庫(kù)配置,如果未啟動(dòng)redis服務(wù)冈在,程序運(yùn)行成功倒慧,但是spring boot默認(rèn)將TokenStore設(shè)置為InMemoryStore,獲取AccessToken也將失敯纫谅!]
話外篇
oauth2-redis-mysql項(xiàng)目中的oauth2-server模塊項(xiàng)目?jī)H在OAuth2服務(wù)器中充當(dāng)認(rèn)證授權(quán)的角色,而一個(gè)完整的OAuth2服務(wù)溅固,則由資源服務(wù)器和認(rèn)證授權(quán)服務(wù)器組成付秕,這兩個(gè)可以合二為一,也可以分開侍郭。后續(xù)我將抽空询吴,編寫OAuth2資源服務(wù)器的搭建,在上述鏈接中已經(jīng)有個(gè)名為oauth2-client的模塊項(xiàng)目亮元,也就是OAuth2資源服務(wù)器猛计,具體使用,稍后再續(xù)爆捞。