驗證流程
也就是說我們的認證中心 以前的話會變成走這樣子的流程
那麼的話就可以發現其實我們的授權中心壓力有點大
網路上找到這幾種流程圖
普通 jwt
1、用戶請求登錄
2、Zuul將請求轉發到授權中心,請求授權
3、授權中心校驗完成,頒發JWT憑證
4、客戶端請求其它功能,攜帶JWT
5、Zuul將JWT交給授權中心校驗,通過後放行
6、用戶請求到達微服務
7、微服務將JWT交給鑑權中心,鑑權同時解析用戶信息
8、鑑權中心返回用戶數據給微服務
9、微服務處理請求,返迴響應
結合 rsa
- 我們首先利用RSA生成公鑰和私鑰。私鑰保存在授權中心,公鑰保存在Zuul和各個微服務
- 用戶請求登錄
- 授權中心校驗,通過後用私鑰對JWT進行簽名加密
- 返回JWT給用戶
- 用戶攜帶JWT訪問
- Zuul直接通過公鑰解密JWT,進行驗證,驗證通過則放行
- 請求到達微服務,微服務直接用公鑰解析JWT,獲取用戶信息,無需訪問授權中心
可能只會有共同一組密鑰問題,不過這樣就不能進行分開鑑權了。
那我們只能這樣動刀了
直接把 ehcache 換成 redis
那麼授權中心就負責頒發 jwt ,在zuul網關訪問服務的時候我們可以把緩存在 redis 的 password 和 username 拿來查詢
假設查詢到代表在頒發jwt的時候 將會進行加密,用自己的密碼去加密,解密的時候將會解碼我們的jwt token 去解碼中間payloadJson 取得 我們的 username 再來就是拿我們的 username 再去查詢我們的 redis 看有沒有緩存,有的話代表已經登入過 那麼我們就可以直接去用我們的 username
if (! JwtUtil.verify(token, username, userDetails.getPassword()))
拿我們傳過來的 token 配合 剛剛查詢的 username 在去從我們緩存在 userDetails 裡面的 password 再去重簽一次
也就是說你偽造jwt也沒用,假設你沒登入過,你就不會緩存在 redis,你的token 又是透過你的 password 去簽的
所以 偽造jwt 要先猜對你的 password 然後又要在你猜對帳號然後又已經登入帳號的時候 也就是 redis seession時間存活時,這樣才能剛好進去。
SecurityConfiguration 新增 bean
- 新增 RedisTemplate
- 重寫我們的 SpringCacheBasedUserCache 為 springzzz
- 重寫 UserDetails 為 CustomUserDetails
- 傳入 RedisTemplate 去調用我們的 redis
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@Autowired
private CacheManager cacheManager;
@Bean
public RedisTemplate<String, CustomUserDetails> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, CustomUserDetails> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.registerModule(new SimpleModule().addDeserializer(
SimpleGrantedAuthority.class, new SimpleGrantedAuthorityDeserializer()));
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Autowired
private RedisTemplate<String, CustomUserDetails> redisTemplate;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
UserCache userCache = new springzz (cacheManager.getCache("jwt-cache"), redisTemplate);
cachingUserDetailsService.setUserCache(userCache);
System.out.println("test");
auth.eraseCredentials(false);
auth.userDetailsService(cachingUserDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {
Constructor<CachingUserDetailsService> ctor = null;
try {
ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
ctor.setAccessible(true);
return BeanUtils.instantiateClass(ctor, delegate);
}
}
SimpleGrantedAuthorityDeserializer
重寫 反向序列SimpleGrantedAuthorityDeserializer
class SimpleGrantedAuthorityDeserializer extends StdDeserializer<SimpleGrantedAuthority> {
public SimpleGrantedAuthorityDeserializer() {
super(SimpleGrantedAuthority.class);
}
@Override
public SimpleGrantedAuthority deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode tree = p.getCodec().readTree(p);
return new SimpleGrantedAuthority(tree.get("authority").textValue());
}
}
springzzzz
使用構造好的 redistemplate
這邊主要是控制 cache
redisTemplate.opsForValue().set(customUserDetails.getUsername(),customUserDetails);
package org.inlighting.security.security;
public class springzz implements UserCache {
public static final Log logger = LogFactory.getLog(springzz.class);
public final Cache cache;
public final RedisTemplate<String, CustomUserDetails> redisTemplate;
public springzz(Cache cache, RedisTemplate<String, CustomUserDetails> redisTemplate) throws Exception {
Assert.notNull(cache, "cache mandatory");
Assert.notNull(redisTemplate, "redisTemplate mandatory");
this.redisTemplate = redisTemplate;
this.cache = cache;
}
public UserDetails getUserFromCache(String username) {
Cache.ValueWrapper element = username != null ? cache.get(username) : null;
if (logger.isDebugEnabled()) {
logger.debug("Cache hit: " + (element != null) + "; username: " + username);
}
if (element == null) {
System.out.println("no here"+username);
return null;
}
else {
System.out.println("im here");
CustomUserDetails result = (CustomUserDetails) redisTemplate.opsForValue().get(username);
System.out.println(result.getUsername());
return (UserDetails) result;
}
}
public void putUserInCache(UserDetails user) {
if (logger.isDebugEnabled()) {
logger.debug("Cache put: " + user.getUsername());
}
CustomUserDetails customUserDetails = new CustomUserDetails (user.getUsername(),user.getPassword(),user.getAuthorities());
System.out.println(customUserDetails.getUsername());
System.out.println(customUserDetails.getAuthorities());
redisTemplate.opsForValue().set(customUserDetails.getUsername(),customUserDetails);
}
public void removeUserFromCache(UserDetails user) {
if (logger.isDebugEnabled()) {
logger.debug("Cache remove: " + user.getUsername());
}
this.removeUserFromCache(user.getUsername());
}
public void removeUserFromCache(String username) {
cache.evict(username);
}
}
重寫後預設UserDetails
CustomUserDetails
@JsonSerialize
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomUserDetails extends Admin implements UserDetails {
public CustomUserDetails() {
super();
}
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super( username, password, authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> listGrantedAuth = new ArrayList<>();
super.getAuthorities().forEach(auth -> {
listGrantedAuth.add(new SimpleGrantedAuthority(auth.toString()));
});
return listGrantedAuth;
}
@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 String getPassword() {
return super.getPassword();
}
@Override
public String getUsername() {
return super.getUsername();
}
}
admin
public class Admin implements Serializable {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public Admin() {
super();
}
public Admin(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super();
this.username = username;
this.password = password;
this.authorities = authorities;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
}
分析redis 分別存入重寫 CustomUserDetails 原本與預設UserDetails
原本與預設
UserDetails
[“org.springframework.security.core.userdetails.User”,{“password”:"$2a10AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",“username”:“jack”,“authorities”:[“java.util.Collections$UnmodifiableSet”,[[“org.springframework.security.core.authority.SimpleGrantedAuthority”,{“authority”:“ROLE_USER”}]]],“accountNonExpired”:true,“accountNonLocked”:true,“credentialsNonExpired”:true,“enabled”:true}]
CustomUserDetails
[“org.inlighting.security.security.CustomUserDetails”,{“username”:“jack”,“password”:"$2a10AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",“authorities”:[“java.util.ArrayList”,[[“org.springframework.security.core.authority.SimpleGrantedAuthority”,{“authority”:“ROLE_USER”}]]],“enabled”:true,“accountNonLocked”:true,“accountNonExpired”:true,“credentialsNonExpired”:true}]
預設 UserDetails
Could not read JSON: Cannot construct instance of org.springframework.security.core.authority.SimpleGrantedAuthority
(although at least one Creator exists):
No Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator
報錯
分別原因
construct 忘記加 建構元
-
到此我們的 改動 ehcache 就改為 redis 接下來就是看 RedisTemplate 如何去讀 redis集群 (應該蠻簡單?
也就是說我們把 db 的 二級緩存 ehcache 換成我們的 redis
新的問題來,我們的認證中心,是否可以換成集群 ,只要確保我們的 redis session 共享就可以了吧! 過幾天再來重構成 集群版。