參照上次 Spring Security + JWT 的簡單應用
一、建立一個Springboot項目,最終的項目結構如下
二、添加pom依賴
<!-- OAuth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
三、習慣性修改yml
server:
port: 6008
四、建立好一個預設的文件
1. 常量表
package com.chris.oauth.consts;
/**
* create by: Chris Chan
* create on: 2019/9/26 8:08
* use for: 全局常量
*/
public interface AppConstants {
//JWT默認指紋
String JWT_SECRET_KEY_NORMAL="JKHDWNCJKLFKKJHDKEKLLDKJKLFHJKHSGHAJKFJLNKSFLLKDLKL";
//響應頭Authorization
String KEY_AUTHORIZATION="Authorization";
}
2. 用戶信息實體
package com.chris.oauth.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* create by: Chris Chan
* create on: 2019/9/26 8:09
* use for:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private String username;
private String password;
}
五、編寫與Security相關的幾個文件
1. SecurityConfig配置
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
/**
* create by: Chris Chan
* create on: 2019/10/12 2:13
* use for:
*/
@Configuration
@EnableWebSecurity
@EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userService)
.passwordEncoder(passwordEncoder);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
在這個配置文件中放行"/oauth/token"接口URL,這是我們oauth需要用到的主要接口。這個配置文件會在oauth2的配置文件之前執行。
2. UserService
package com.chris.oauth.service;
import com.chris.oauth.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* create by: Chris Chan
* create on: 2019/10/11 12:31
* use for:
*/
@Service
public class UserService implements UserDetailsService {
private static Map<String, String> userMap = new HashMap<>(16);//用戶
private static Map<String, String> userAuthMap = new HashMap<>(16);//用戶權限
private static Map<String, String> userAdditionalMap = new HashMap<>(16);//用戶附加信息 測試
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
return null;
}
UserInfo userInfo = findUser(username);
if (null == userInfo) {
return null;
}
return new User(username, userInfo.getPassword(), getAuthorityList(username));
}
/**
* 返回密碼
* 這個方法可以假設是從數據庫dao層獲取到用戶信息
*
* @param username
* @return
*/
private UserInfo findUser(String username) {
if (null == userMap) {
userMap = new HashMap<>(16);
}
//內置幾個用戶
if (userMap.size() == 0) {
userMap.put("zhangsanfeng", passwordEncoder.encode("123123"));
userMap.put("lisifu", passwordEncoder.encode("123123"));
userMap.put("songzihao", passwordEncoder.encode("123123"));
}
String password = userMap.get(username);
if (StringUtils.isEmpty(password)) {
return null;
}
return new UserInfo(username, password);
}
/**
* 獲取用戶權限
* 這個方法也可以在數據庫中查詢
*
* @param username
* @return
*/
public static String[] getAuthorities(String username) {
if (null == userAuthMap) {
userAuthMap = new HashMap<>(16);
}
//內置幾個用戶權限
if (userAuthMap.size() == 0) {
userAuthMap.put("zhangsanfeng", "ROLE_ADMIN,ROLE_USER");
userAuthMap.put("lisifu", "ROLE_ADMIN,ROLE_USER");
userAuthMap.put("songzihao", "ROLE_SYS,ROLE_ADMIN,ROLE_USER");
}
return userAuthMap.get(username).split(",");
}
/**
* 獲取用戶權限集合
*
* @param username
* @return
*/
public static List<GrantedAuthority> getAuthorityList(String username) {
return AuthorityUtils.createAuthorityList(getAuthorities(username));
}
/**
* 獲取用戶的附加信息 測試使用 用以擴展JWT
*
* @param username
* @return
*/
public static String getUserAdditional(String username) {
if (null == userAdditionalMap) {
userAdditionalMap = new HashMap<>(16);
}
//內置幾個用戶權限
if (userAdditionalMap.size() == 0) {
userAdditionalMap.put("zhangsanfeng", "worker");
userAdditionalMap.put("lisifu", "student");
userAdditionalMap.put("songzihao", "teacher");
}
return userAdditionalMap.get(username);
}
}
增加了一個用戶附加信息,這些在實際業務中都是可以從數據庫中去查詢的,測試目的是擴展jwt的信息。
六、有關的Bean的管理,由於Bean比較多,所以我們把他們收集起來管理。
package com.chris.oauth.config;
import com.chris.oauth.consts.AppConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* create by: Chris Chan
* create on: 2019/10/12 12:35
* use for:
*/
@Configuration
public class BeanBox {
/**
* 密碼編碼器
* 這個實例中security和oauth都是用一個密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* jwt生成處理
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* token轉換器
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
// converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
converter.setSigningKey(AppConstants.JWT_SECRET_KEY_NORMAL);//設置JWT指紋
return converter;
}
}
七、JWT擴展器
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* create by: Chris Chan
* create on: 2019/10/12 12:39
* use for: JWT擴展處理
*/
@Component
@Primary
public class JWTTokenEnhancer implements TokenEnhancer {
@Autowired
UserService userService;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
//添加附加信息
Map<String, Object> map = new HashMap<>(16);
map.put("additional", userService.getUserAdditional(username));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
//設置過期時間
((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(LocalDateTime.now().plusDays(1).toInstant(ZoneOffset.of("+8")).toEpochMilli()));
return accessToken;
}
}
默認的JwtTokenStore產生的jwt只包含用戶名和權限列表,如果我們需要爲登錄用戶在jwt中添加其他的必要信息,就要編寫這個。
八、與OAuth2相關的兩個配置文件
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.ArrayList;
import java.util.List;
/**
* create by: Chris Chan
* create on: 2019/10/12 2:15
* use for:
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserService userService;
@Autowired
TokenStore tokenStore;
@Autowired
TokenEnhancer tokenEnhancer;
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private AuthenticationManager authenticationManager;
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("clientId")
.secret(passwordEncoder.encode("clientSecret"))
.redirectUris("http://localhost:6008/callback")
.authorizedGrantTypes("authorization_code", "password", "implicit", "client_credentials", "refresh_token")
.resourceIds("resId")
.scopes("scope1", "scope2");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
.userDetailsService(userService)
.authenticationManager(authenticationManager);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(tokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints.tokenEnhancer(enhancerChain)
.accessTokenConverter(jwtAccessTokenConverter);
}
}
租後一個方法就是配置jwt和擴展工具等等。
package com.chris.oauth.config;
import org.springframework.context.annotation.Configuration;
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;
/**
* create by: Chris Chan
* create on: 2019/9/27 16:51
* use for:
*/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
}
九、測試
程序運行起來,我們用PostMan來測試
1. 把所有參數全部添加到url中:
localhost:6008/oauth/token?grant_type=password&username=songzihao&password=123123&client_id=clientId&client_secret=clientSecret
因爲/oauth/token是post請求方式,不可以直接在瀏覽器地址欄請求。
結果:
2. client_id和client_secret不出現在url中,而是用basic auth的方式提交
結果相同。
3. 把client_id和client_secret用英文冒號連接,進行base64url編碼,然後放到頭部信息Authorization中,加Basic 前綴。
得到正確的結果。
4. 我們再把訪問token中有效載荷部分解析一下,會看到我們放置在其中的用戶名、權限列表、附加信息,client_id,還有我們自己設置的過期時間
有效時間被我設置爲一天。
本次測試圓滿完成。
九、填坑記
1. 本次主要目的是測試password模式,不過在加上配置文件之後,password模式必須要AuthenticationManager的配置;
2. 在password模式下,總是出現很多次調用太深的error,我把yml中配置的用戶名和密碼去掉,在內存中放置的用戶信息,並且指定了密碼編碼器才得以解決。
十、說明
在分佈式中,oauth2只是用來提供jwt,分佈式業務服務中,是接收訪問令牌並加以解析,同樣的,需要提供相同的JWT資料,比如指紋,或者祕鑰文件。