搭建一個oauth2服務器,包括認證、受權和資源服務器html
參考資料:前端
www.cnblogs.com/fp2952/p/89…java
Spring OAuth2官方文檔github
本文分爲兩個部分web
項目地址:github.com/zheyday/Spr…算法
oauth分支spring
使用Spring Initializr新建項目,勾選以下三個選項數據庫
pom.xml瀏覽器
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//只須要引用這一個
//集成了spring-security-oauth2 spring-security-jwt spring-security-oauth2-autoconfigure
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
複製代碼
新建類WebSecurityConfig 繼承 WebSecurityConfigurerAdapter,並添加@Configuration @EnableWebSecurity註解,重寫三個方法,代碼以下,詳細講解在代碼下面
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
//內存存儲
// auth
// .inMemoryAuthentication()
// .passwordEncoder(passwordEncoder())
// .withUser("user")
// .password(passwordEncoder().encode("user"))
// .roles("USER");
}
/** * 配置了默認表單登錄以及禁用了 csrf 功能,並開啓了httpBasic 認證 * * @param http * @throws Exception */
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登錄頁/login並容許訪問
.formLogin().permitAll()
// 登出頁
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 其他全部請求所有須要鑑權認證
.and().authorizeRequests().anyRequest().authenticated()
// 因爲使用的是JWT,咱們這裏不須要csrf
.and().csrf().disable();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
複製代碼
主要講解一下
protected void configure(AuthenticationManagerBuilder auth) throws Exception 複製代碼
這個方法是用來驗證用戶信息的。將前端輸入的用戶名和密碼與數據庫匹配,若是有這個用戶才能認證成功。咱們注入了一個UserServiceDetail
,這個service的功能就是驗證。.passwordEncoder(passwordEncoder())
是使用加鹽解密。
UserServiceDetail
實現了UserDetailsService
接口,因此須要實現惟一的方法
package zcs.oauthserver.service;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 zcs.oauthserver.model.UserModel;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceDetail implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE"));
return new UserModel("user","user",authorities);
}
}
複製代碼
這裏先用假參數實現功能,後面添加數據庫
參數s是前端輸入的用戶名,經過該參數查找數據庫,獲取密碼和角色權限,最後將這三個數據封裝到UserDetails
接口的實現類中返回。這裏封裝的類可使用org.springframework.security.core.userdetails.User
或者本身實現UserDetails
接口。
UserModel
實現UserDetails
接口
package zcs.oauthserver.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Collection;
import java.util.List;
public class UserModel implements UserDetails {
private String userName;
private String password;
private List<SimpleGrantedAuthority> authorities;
public UserModel(String userName, String password, List<SimpleGrantedAuthority> authorities) {
this.userName = userName;
this.password = new BCryptPasswordEncoder().encode(password);;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
複製代碼
新增username、password和authorities,最後一個存儲的是該用戶的權限列表,也就是用戶擁有可以訪問哪些資源的權限。密碼加鹽處理。
新建配置類AuthorizationServerConfig 繼承 AuthorizationServerConfigurerAdapter,並添加@Configuration @EnableAuthorizationServer註解代表是一個認證服務器
重寫三個函數
ClientDetailsServiceConfigurer
:用來配置客戶端詳情服務,客戶端詳情信息在這裏進行初始化,你可以把客戶端詳情信息寫死在這裏或者是經過數據庫來存儲調取詳情信息。客戶端就是指第三方應用AuthorizationServerSecurityConfigurer
:用來配置令牌端點(Token Endpoint)的安全約束.AuthorizationServerEndpointsConfigurer
:用來配置受權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)。@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//從WebSecurityConfig加載
@Autowired
private AuthenticationManager authenticationManager;
//內存存儲令牌
private TokenStore tokenStore = new InMemoryTokenStore();
/** * 配置客戶端詳細信息 * * @param clients * @throws Exception */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客戶端ID
.withClient("zcs")
.secret(new BCryptPasswordEncoder().encode("zcs"))
//權限範圍
.scopes("app")
//受權碼模式
.authorizedGrantTypes("authorization_code")
//隨便寫
.redirectUris("www.baidu.com");
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager);
}
/** * 在令牌端點定義安全約束 * 容許表單驗證,瀏覽器直接發送post請求便可獲取tocken * 這部分寫這樣就行 * @param security * @throws Exception */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 開啓/oauth/token_key驗證端口無權限訪問
.tokenKeyAccess("permitAll()")
// 開啓/oauth/check_token驗證端口認證權限訪問
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
}
複製代碼
客戶端詳細信息一樣也是測試用,後續會加上數據庫。令牌服務暫時是用內存存儲,後續加上jwt。
先實現功能最重要,複雜的東西一步步往上加。
資源服務器也就是服務程序,是須要訪問的服務器
新建ResourceServerConfig繼承ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
// antMatcher表示只能處理/user的請求
.antMatcher("/user/**")
.authorizeRequests()
.antMatchers("/user/test1").permitAll()
.antMatchers("/user/test2").authenticated()
// .antMatchers("user/test2").hasRole("USER")
// .anyRequest().authenticated()
;
}
}
複製代碼
ResourceServerConfigurerAdapter
的Order默認值是3,小於WebSecurityConfigurerAdapter
,值越小優先級越大
關於ResourceServerConfigurerAdapter
和WebSecurityConfigurerAdapter
的詳細說明見
新建UserController
@RestController
public class UserController {
@GetMapping("/user/me")
public Principal user(Principal principal) {
return principal;
}
@GetMapping("/user/test1")
public String test() {
return "test1";
}
@GetMapping("/user/test2")
public String test2() {
return "test2";
}
}
複製代碼
http://127.0.0.1:9120/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=www.baidu.com
,而後跳出登錄頁面,地址欄會出現回調頁面,而且帶有code參數 http://127.0.0.1:9120/oauth/www.baidu.com?code=FGQ1jg
http://127.0.0.1:9120/oauth/token?code=FGQ1jg&grant_type=authorization_code&redirect_uri=www.baidu.com&client_id=zcs&client_secret=zcs
,code填寫剛纔獲得的code,使用POST請求
有不少人會把JWT和OAuth2來做比較,其實它倆是徹底不一樣的概念,沒有可比性。
JWT是一種認證協議,提供一種用於發佈接入令牌、並對發佈的簽名接入令牌進行驗證的方法。
OAuth2是一種受權框架,提供一套詳細的受權機制。
Spring Cloud OAuth2集成了JWT做爲令牌管理,所以使用起來很方便
JwtAccessTokenConverter
是用來生成token的轉換器,而token令牌默認是有簽名的,且資源服務器須要驗證這個簽名。此處的加密及驗籤包括兩種方式: 對稱加密、非對稱加密(公鑰密鑰) 對稱加密須要受權服務器和資源服務器存儲同一key值,而非對稱加密可以使用密鑰加密,暴露公鑰給資源服務器驗籤,本文中使用非對稱加密方式。
經過jdk工具生成jks證書,經過cmd進入jdk安裝目錄的bin下,運行命令
keytool -genkeypair -alias oauth2-keyalg RSA -keypass mypass -keystore oauth2.jks -storepass mypass
會在當前目錄生成oauth2.jks文件,放入resource目錄下。
maven默認不加載resource目錄下的文件,因此須要在pom.xml中配置,在build下添加配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
複製代碼
在原來的AuthorizationServerConfig中更改部分代碼
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints.tokenStore(tokenStore)
// .authenticationManager(authenticationManager);
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(tokenStore);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/** * 非對稱加密算法對token進行簽名 * @return */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
// 導入證書
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
return converter;
}
複製代碼
jwtAccessTokenConverter
方法中有一個CustomJwtAccessTokenConverter
類,這是繼承了JwtAccessTokenConverter
,自定義添加了額外的token信息
/** * 自定義添加額外token信息 */
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> additionalInfo = new HashMap<>();
UserModel user = (UserModel)authentication.getPrincipal();
additionalInfo.put("USER",user);
defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
return super.enhance(defaultOAuth2AccessToken,authentication);
}
}
複製代碼
以前登錄是用假數據,如今經過鏈接數據庫進行驗證。
創建三個表,user存儲用戶帳號和密碼,role存儲角色,user_role存儲用戶的角色
user表
role表
user_role表
使用mybatis-plus生成代碼,改造以前的UserServiceDetail
和UserModel
UserServiceDetail
@Service
public class UserServiceDetail implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Autowired
public UserServiceDetail(UserMapper userMapper, RoleMapper roleMapper) {
this.userMapper = userMapper;
this.roleMapper = roleMapper;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username", s);
User user = userMapper.selectOne(userQueryWrapper);
if (user == null) {
throw new RuntimeException("用戶名或密碼錯誤");
}
user.setAuthorities(roleMapper.selectByUserId(user.getId()));
return user;
}
}
複製代碼
經過UserMapper查詢用戶信息,而後封裝到User中,在自動生成的User上實現UserDetails接口
User
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableId(value = "username")
private String username;
@TableId(value = "password")
private String password;
@TableField(exist = false)
private List<Role> authorities;
public User() {
}
public Integer getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = new BCryptPasswordEncoder().encode(password);
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username=" + username +
", password=" + password +
"}";
}
}
複製代碼
解釋說明:
UserDetails中須要重寫一個方法,是存儲用戶權限的
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
複製代碼
因此新增了一個變量,而且打上註解表示這不是一個字段屬性
@TableField(exist = false)
private List<Role> authorities;
複製代碼
在Role上實現GrantedAuthority接口,只須要權限名稱就能夠了
public class Role implements Serializable, GrantedAuthority {
private static final long serialVersionUID = 1L;
private String name;
@Override
public String toString() {
return name;
}
@Override
public String getAuthority() {
return name;
}
}
複製代碼
在RoleMapper.java中新增方法,經過用戶id查詢擁有的角色
@Select("select name from role r INNER JOIN user_role ur on ur.user_id=1 and ur.role_id=r.id")
List<Role> selectByUserId(Integer id);
複製代碼
測試方法和第一部分同樣,獲取令牌的時候返回以下
參考連接:
juejin.im/post/5c5ae6… 更多文章見我的博客 zheyday.github.io/