瘋狂創客圈 Java 高並發【 億級流量聊天室實戰】實戰系列 【博客園總入口 】
架構師成長+面試必備之 高並發基礎書籍 【Netty Zookeeper Redis 高並發實戰 】
前言
Crazy-SpringCloud 微服務腳手架 &視頻介紹:
Crazy-SpringCloud 微服務腳手架,是為 Java 微服務開發 入門者 准備的 學習和開發腳手架。並配有一系列的使用教程和視頻,大致如下:
高並發 環境搭建 圖文教程和演示視頻,陸續上線:
中間件 | 鏈接地址 |
---|---|
Linux Redis 安裝(帶視頻) | Linux Redis 安裝(帶視頻) |
Linux Zookeeper 安裝(帶視頻) | Linux Zookeeper 安裝, 帶視頻 |
Windows Redis 安裝(帶視頻) | Windows Redis 安裝(帶視頻) |
RabbitMQ 離線安裝(帶視頻) | RabbitMQ 離線安裝(帶視頻) |
ElasticSearch 安裝, 帶視頻 | ElasticSearch 安裝, 帶視頻 |
Nacos 安裝(帶視頻) | Nacos 安裝(帶視頻) |
Crazy-SpringCloud 微服務腳手架 圖文教程和演示視頻,陸續上線:
組件 | 鏈接地址 |
---|---|
Eureka | Eureka 入門,帶視頻 |
SpringCloud Config | springcloud Config 入門,帶視頻 |
spring security | spring security 原理+實戰 |
Spring Session | SpringSession 獨立使用 |
分布式 session 基礎 | RedisSession (自定義) |
重點: springcloud 開發腳手架 | springcloud 開發腳手架 |
SpingSecurity + SpringSession 死磕 (寫作中) | SpingSecurity + SpringSession 死磕 |
小視頻以及所需工具的百度網盤鏈接,請參見 瘋狂創客圈 高並發社群 博客
Spring Security 的重要性
在web應用開發中,安全無疑是十分重要的,選擇Spring Security來保護web應用是一個非常好的選擇。Spring Security 是spring項目之中的一個安全模塊,特別是在spring boot項目中,spring security已經默認集成和啟動了。
Spring Security 默認為自動開啟的,可見其重要性。
如果要關閉,需要在啟動類加上,exclude ={SecurityAutoConfiguration} 的配置
@EnableEurekaClient
@SpringBootApplication(scanBasePackages = {
"com.crazymaker.springcloud.user",
"com.crazymaker.springcloud.seckill.remote.fallback",
"com.crazymaker.springcloud.standard"
}, exclude = {SecurityAutoConfiguration.class})
或者
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
一般不建議關閉。
Spring Security 核心組件
spring security核心組件有:Userdetails 、Authentication,UserDetailsService、AuthenticationProvider、AuthenticationManager 下面分別介紹。
Authentication
authentication 直譯過來是“認證”的意思,在Spring Security 中Authentication用來表示當前用戶是誰,一般來講你可以理解為authentication就是一組用戶名密碼信息。Authentication也是一個接口,其定義如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
接口有4個get方法,分別獲取
-
Authorities, 填充的是用戶角色信息。
-
Credentials,直譯,證書。填充的是密碼。
-
Details ,用戶信息。
-
Principal 直譯,形容詞是“主要的,最重要的”,名詞是“負責人,資本,本金”。感覺很別扭,所以,還是不翻譯了,直接用原詞principal來表示這個概念,其填充的是用戶名。
因此可以推斷其實現類有這4個屬性。
這幾個方法作用如下:
-
getAuthorities: 獲取用戶權限,一般情況下獲取到的是用戶的角色信息。
-
getCredentials: 獲取證明用戶認證的信息,通常情況下獲取到的是密碼等信息。
-
getDetails: 獲取用戶的額外信息,(這部分信息可以是我們的用戶表中的信息)
-
getPrincipal: 獲取用戶身份信息,在未認證的情況下獲取到的是用戶名,在已認證的情況下獲取到的是 UserDetails (UserDetails也是一個接口,里邊的方法有getUsername,getPassword等)。
-
isAuthenticated: 獲取當前 Authentication 是否已認證。
-
setAuthenticated: 設置當前 Authentication 是否已認證(true or false)。
UserDetails
UserDetails,看命知義,是用戶信息的意思。其存儲的就是用戶信息,其定義如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
方法含義如下:
-
getAuthorites:獲取用戶權限,本質上是用戶的角色信息。
-
getPassword: 獲取密碼。
-
getUserName: 獲取用戶名。
-
isAccountNonExpired: 賬戶是否過期。
-
isAccountNonLocked: 賬戶是否被鎖定。
-
isCredentialsNonExpired: 密碼是否過期。
-
isEnabled: 賬戶是否可用。
UserDetailsService
提到了UserDetails就必須得提到UserDetailsService, UserDetailsService也是一個接口,且只有一個方法loadUserByUsername,他可以用來獲取UserDetails。
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
通常在spring security應用中,我們會自定義一個CustomUserDetailsService來實現UserDetailsService接口,並實現其public UserDetails loadUserByUsername(final String login);方法。我們在實現loadUserByUsername方法的時候,就可以通過查詢數據庫(或者是緩存、或者是其他的存儲形式)來獲取用戶信息,然后組裝成一個UserDetails,(通常是一個org.springframework.security.core.userdetails.User,它繼承自UserDetails) 並返回。
在實現loadUserByUsername方法的時候,如果我們通過查庫沒有查到相關記錄,需要拋出一個異常來告訴spring security來“善后”。這個異常是org.springframework.security.core.userdetails.UsernameNotFoundException。
AuthenticationProvider
負責真正的驗證。
當我們使用 authentication-provider 元素來定義一個 AuthenticationProvider 時,如果沒有指定對應關聯的 AuthenticationProvider 對象,Spring Security 默認會使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在進行認證的時候需要一個 UserDetailsService 來獲取用戶的信息 UserDetails,其中包括用戶名、密碼和所擁有的權限等。所以如果我們需要改變認證的方式,我們可以實現自己的 AuthenticationProvider;如果需要改變認證的用戶信息來源,我們可以實現 UserDetailsService。
實現了自己的 AuthenticationProvider 之后,我們可以在配置文件中這樣配置來使用我們自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我們自己的 AuthenticationProvider 實現類對應的 bean。
AuthenticationProvider 接口如下:
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
-
authenticate 表示認證的動作。
-
supports 表示所支持的 Authentication類型。Authentication 包含很多子類,如果 AbstractAuthenticationToken 。
AbstractAuthenticationToken implements Authentication
還有,可以自定義 Authentication ,比如 本實例所使用的: JwtAuthenticationToken。
AuthenticationManager
認證是由 AuthenticationManager 來管理的,但是真正進行認證的是 AuthenticationManager 中定義的 AuthenticationProvider。AuthenticationManager 中可以定義有多個 AuthenticationProvider。
AuthenticationManager 是一個接口,它只有一個方法,接收參數為Authentication,其定義如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校驗Authentication,如果驗證失敗會拋出AuthenticationException異常。AuthenticationException是一個抽象類,因此代碼邏輯並不能實例化一個AuthenticationException異常並拋出,實際上拋出的異常通常是其實現類,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能會比較常見,即密碼錯誤的時候。
組件比較多,但是如果主要流程理順了,也比較簡單。
Spring Security 實戰
搞定兩個 AuthenticationProvider:
(1) 從數據庫獲取用戶
首先通過 UserDetailsService 獲取 UserDetails,然后 通過 UserDetailsService 裝配 DaoAuthenticationProvider
(2) 完成用戶的認證
實現一個自己的 JwtAuthenticationProvider,完成用戶的認證
(3)定制一個過濾器
(4)完成所有組件的裝配
實戰1 : UserDetailsService 獲取 UserDetails
首先通過 UserDetailsService 獲取 UserDetails,然后 通過 UserDetailsService 裝配 DaoAuthenticationProvider。
package com.crazymaker.springcloud.user.info.service.impl;
@Slf4j
@Service
public class UserAuthService implements UserDetailsService {
private PasswordEncoder passwordEncoder;
public UserAuthService() {
//默認使用 bcrypt, strength=10
this.passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
private UserPO loadFromDB(String username) {
if (null == userDao)
{
userDao = CustomAppContext.getBean(UserDao.class);
}
List<UserPO> list = userDao.findAllByLoginName(username);
if (null == list || list.size() <= 0) {
return null;
}
UserPO userPO = list.get(0);
return userPO;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserPO userPO = loadFromDB(username);
//將salt放到password字段返回
return User.builder()
.username(userPO.getLoginName())
.password(userPO.getPassword())
// .password(SessionConstants.SALT)
//BCrypt.gensalt(); 正式開發時可以調用該方法實時生成加密的salt
// .password(SessionConstants.SALT)
.authorities(SessionConstants.USER_INFO)
.roles("USER")
.build();
}
}
實戰2: 裝配 DaoAuthenticationProvider
在 SecurityConfiguration 配置類中加入如下內容:
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//這里會默認使用BCryptPasswordEncoder比對加密后的密碼,注意要跟createUser時保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
}
@Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
}
實戰3: 實現一個自己的 JwtAuthenticationProvider
繼承於 AuthenticationProvider,實現一個自己的 JwtAuthenticationProvider,完成用戶的認證
package com.crazymaker.springcloud.standard.security.provider;
//...
public class JwtAuthenticationProvider implements AuthenticationProvider {
private RedisOperationsSessionRepository sessionRepository;
private CustomedSessionIdResolver httpSessionIdResolver;
public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository,
CustomedSessionIdResolver httpSessionIdResolver) {
this.sessionRepository = sessionRepository;
this.httpSessionIdResolver = httpSessionIdResolver;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken();
if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) {
throw new NonceExpiredException("認證過期");
}
String sid = jwt.getSubject();
String otoken = jwt.getToken();
Session session = null;
try {
session = sessionRepository.findById(sid);
} catch (Exception e) {
e.printStackTrace();
}
if (null == session) {
throw new NonceExpiredException("認證有誤,請重新登錄");
}
String json = session.getAttribute(G_USER);
if (StringUtils.isBlank(json)) {
throw new NonceExpiredException("認證有誤,請重新登錄");
}
UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
if (null == userDTO) {
throw new NonceExpiredException("認證有誤");
}
String password = userDTO.getPassword();
String username = userDTO.getLoginName();
UserDetails user = User.builder()
.username(username)
.password(password)
.authorities(SessionConstants.USER_INFO)
.build();
String encryptSalt = password;
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(sid).build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
JwtAuthenticationToken token =
new JwtAuthenticationToken(user, jwt, user.getAuthorities());
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
實戰4: 裝配 AuthenticationManager
認證是由 AuthenticationManager 來管理的,但是真正進行認證的是 AuthenticationManager 中定義的 AuthenticationProvider。AuthenticationManager 中可以定義有多個 AuthenticationProvider。
@EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
//....
}
實戰5: 定制過濾器,將 AuthenticationManager 用起來
搞得再多,如果不通過過濾器,將 AuthenticationManager 用起來,也是沒有用的。
package com.crazymaker.springcloud.standard.security.filter;
//.....
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private RequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//.....
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authResult = null;
/**
* 場景: 從 zuul 過來,直接帶上session 頭
*/
if (StringUtils.isNotEmpty(request.getHeader(SessionConstants.SESSION_SEED))) {
request.setAttribute(SessionConstants.SESSION_SEED,
request.getHeader(SessionConstants.SESSION_SEED));
UserDetails userDetails = User.builder()
.username(request.getHeader(SessionConstants.SESSION_SEED))
.password(request.getHeader(SessionConstants.SESSION_SEED))
.authorities(SessionConstants.USER_INFO)
.build();
authResult = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
successfulAuthentication(request, response, filterChain, authResult);
filterChain.doFilter(request, response);
return;
}
/**
* 正常場景: 單體微服務訪問,或者從Zuul過來,沒有帶 session head
*/
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
AuthenticationException failed = null;
try {
String token = getJwtToken(request);
if (StringUtils.isNotBlank(token)) {
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
DecodedJWT jwt = authToken.getToken();
//將 AuthenticationManager 用起來
authResult = this.getAuthenticationManager().authenticate(authToken);
UserDetails user = (UserDetails) authResult.getPrincipal();
request.setAttribute(SessionConstants.SESSION_SEED, jwt.getSubject());
} else {
failed = new InsufficientAuthenticationException("請求頭認證消息為空");
}
} catch (JWTDecodeException e) {
logger.error("JWT format error", e);
failed = new InsufficientAuthenticationException("請求頭認證消息格式錯誤", failed);
} catch (InternalAuthenticationServiceException e) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
failed = e;
} catch (AuthenticationException e) {
// Authentication failed
failed = e;
}
if (authResult != null) {
successfulAuthentication(request, response, filterChain, authResult);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
//....
}
實戰6: 配置 HttpSecurity 的過濾機制
還是在 UserWebSecurityConfig 配置文件,將 HttpSecurity 的過濾機制配置起來,完成所有組件的裝配。
代碼如下:
package com.crazymaker.springcloud.user.info.config;
//...
@EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserAuthService userAuthService;
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/api/user/login/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/say/hello/v1",
// "/api/user/*/detail/v1",
"/api/crazymaker/duty/info/user/login")
.permitAll()
.anyRequest().authenticated()
// .antMatchers("/image/**").permitAll()
// .antMatchers("/admin/**").hasAnyRole("ADMIN")
.and()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
.apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
.and()
.apply(new JwtAuthConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
.and()
.logout()
// .logoutUrl("/logout") //默認就是"/logout"
.addLogoutHandler(tokenClearLogoutHandler())
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.and()
.addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class)
.sessionManagement().disable()
;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/api/user/login/v1",
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
// "/api/user/say/hello/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/*/detail/v1",
"/images/**",
"/swagger-ui.html",
"/webjars/**",
"**/favicon.ico",
"/css/**",
"/js/**",
"/api/crazymaker/info/user/login"
);
}
@Resource
RedisOperationsSessionRepository sessionRepository;
@Resource
public CustomedSessionIdResolver httpSessionIdResolver;
@DependsOn({"sessionRepository", "httpSessionIdResolver"})
@Bean("jwtAuthenticationProvider")
protected AuthenticationProvider jwtAuthenticationProvider() {
return new JwtAuthenticationProvider(sessionRepository, httpSessionIdResolver);
}
public <S extends Session> OncePerRequestFilter springSessionRepositoryFilter() {
CustomedSessionRepositoryFilter<? extends Session> sessionRepositoryFilter = new CustomedSessionRepositoryFilter<>(
sessionRepository);
// sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//這里會默認使用BCryptPasswordEncoder比對加密后的密碼,注意要跟createUser時保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
}
@Bean
protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() {
return new JwtRefreshSuccessHandler();
}
@Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
}
@Bean
protected JsonLoginSuccessHandler jsonLoginSuccessHandler() {
return new JsonLoginSuccessHandler(userAuthService);
}
@Bean
protected TokenClearLogoutHandler tokenClearLogoutHandler() {
return new TokenClearLogoutHandler(userAuthService);
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "HEAD", "OPTION"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.addExposedHeader(SessionConstants.AUTHORIZATION);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
實戰小結
大概通過以上6步,一個集成jwt的springsecurity機制,完整的配置起來了。
具體,請關注 Java 高並發研習社群 【博客園 總入口 】
最后,介紹一下瘋狂創客圈:瘋狂創客圈,一個Java 高並發研習社群 【博客園 總入口 】
瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高並發實戰》
瘋狂創客圈 Java 死磕系列
- Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
- Netty 源碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
- 瘋狂創客圈 【 博客園 總入口 】
Java 面試題 一網打盡**
- 瘋狂創客圈 【 博客園 總入口 】