Spring Security 介紹
Spring Security 應(yīng)該屬于 Spring 全家桶中學(xué)習(xí)曲線比較陡峭的幾個模塊之一核芽,下面我將從起源和定義這兩個方面來簡單介紹一下它铝噩。
起源:?Spring Security 實際上起源于 Acegi Security,這個框架能為基于 Spring 的企業(yè)應(yīng)用提供強(qiáng)大而靈活安全訪問控制解決方案碌冶,并且框架這個充分利用 Spring 的 IoC 和 AOP 功能悦陋,提供聲明式安全訪問控制的功能么鹤。后面案狠,隨著這個項目發(fā)展, Acegi Security 成為了Spring官方子項目卷雕,后來被命名為 “Spring Security”节猿。
**定義:**Spring Security 是一個功能強(qiáng)大且高度可以定制的框架,側(cè)重于為Java 應(yīng)用程序提供身份驗證和授權(quán)爽蝴°迮——官方介紹。
Session 和 Token 認(rèn)證對比
Session 認(rèn)證圖解
很多時候我們都是通過 SessionID 來實現(xiàn)特定的用戶蝎亚,SessionID 一般會選擇存放在 Redis 中九孩。舉個例子:用戶成功登陸系統(tǒng),然后返回給客戶端具有 SessionID 的 Cookie发框,當(dāng)用戶向后端發(fā)起請求的時候會把 SessionID 帶上躺彬,這樣后端就知道你的身份狀態(tài)了。
關(guān)于這種認(rèn)證方式更詳細(xì)的過程如下:
用戶向服務(wù)器發(fā)送用戶名和密碼用于登陸系統(tǒng)梅惯。
服務(wù)器驗證通過后宪拥,服務(wù)器為用戶創(chuàng)建一個 Session,并將 Session信息存儲 起來铣减。
服務(wù)器向用戶返回一個 SessionID她君,寫入用戶的 Cookie。
當(dāng)用戶保持登錄狀態(tài)時葫哗,Cookie 將與每個后續(xù)請求一起被發(fā)送出去缔刹。
服務(wù)器可以將存儲在 Cookie 上的 Session ID 與存儲在內(nèi)存中或者數(shù)據(jù)庫中的 Session 信息進(jìn)行比較,以驗證用戶的身份劣针,返回給用戶客戶端響應(yīng)信息的時候會附帶用戶當(dāng)前的狀態(tài)校镐。
Token 認(rèn)證圖解
在基于 Token 進(jìn)行身份驗證的的應(yīng)用程序中,服務(wù)器通過Payload捺典、Header和一個密鑰(secret)創(chuàng)建令牌(Token)并將 Token 發(fā)送給客戶端鸟廓,客戶端將 Token 保存在 Cookie 或者 localStorage 里面,以后客戶端發(fā)出的所有請求都會攜帶這個令牌襟己。你可以把它放在 Cookie 里面自動發(fā)送引谜,但是這樣不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中: Authorization: Bearer Token擎浴。
關(guān)于這種認(rèn)證方式更詳細(xì)的過程如下:
用戶向服務(wù)器發(fā)送用戶名和密碼用于登陸系統(tǒng)煌张。
身份驗證服務(wù)響應(yīng)并返回了簽名的 JWT,上面包含了用戶是誰的內(nèi)容退客。
用戶以后每次向后端發(fā)請求都在 Header 中帶上 JWT。
服務(wù)端檢查 JWT 并從中獲取用戶相關(guān)信息。
項目涉及到的重要類說明
配置類
在本項目中我們自定義 SecurityConfig 繼承了 WebSecurityConfigurerAdapter萌狂。 WebSecurityConfigurerAdapter提供HttpSecurity來配置 cors档玻,csrf,會話管理和受保護(hù)資源的規(guī)則茫藏。
配置類中我們主要配置了:
密碼編碼器 BCryptPasswordEncoder(存入數(shù)據(jù)庫的密碼需要被加密)误趴。
為 AuthenticationManager 設(shè)置自定義的 UserDetailsService以及密碼編碼器;
在 Spring Security 配置指定了哪些路徑下的資源需要驗證了的用戶才能訪問务傲、哪些不需要以及哪些資源只能被特定角色訪問凉当;
將我們自定義的兩個過濾器添加到 Spring Security 配置中;
將兩個自定義處理權(quán)限認(rèn)證方面的異常類添加到 Spring Security 配置中售葡;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 密碼編碼器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService createUserDetailsService() {
return userDetailsServiceImpl;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 設(shè)置自定義的userDetailsService以及密碼編碼器
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
// 禁用 CSRF
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/auth/login").permitAll()
// 指定路徑下的資源需要驗證了的用戶才能訪問
.antMatchers("/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
//添加自定義Filter
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session(不創(chuàng)建會話)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授權(quán)異常處理
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.accessDeniedHandler(new JWTAccessDeniedHandler());
}
}
跨域:
在這里踩的一個坑是:如果你沒有設(shè)置exposedHeaders("Authorization")暴露 header 中的"Authorization"屬性給客戶端應(yīng)用程序的話看杭,前端是獲取不到 token 信息的。
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
//暴露header中的其他屬性給客戶端應(yīng)用程序
//如果不設(shè)置這個屬性前端無法通過response header獲取到Authorization也就是token
.exposedHeaders("Authorization")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
工具類
/**
* @author shuang.kou
*/
public class JwtTokenUtils {
/**
* 生成足夠的安全隨機(jī)密鑰挟伙,以適合符合規(guī)范的簽名
*/
private static byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);
private static SecretKey secretKey = Keys.hmacShaKeyFor(apiKeySecretBytes);
public static String createToken(String username, List roles, boolean isRememberMe) {
long expiration = isRememberMe ? SecurityConstants.EXPIRATION_REMEMBER : SecurityConstants.EXPIRATION;
String tokenPrefix = Jwts.builder()
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
.signWith(secretKey, SignatureAlgorithm.HS256)
.claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles))
.setIssuer("SnailClimb")
.setIssuedAt(new Date())
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
return SecurityConstants.TOKEN_PREFIX + tokenPrefix;
}
private boolean isTokenExpired(String token) {
Date expiredDate = getTokenBody(token).getExpiration();
return expiredDate.before(new Date());
}
public static String getUsernameByToken(String token) {
return getTokenBody(token).getSubject();
}
/**
* 獲取用戶所有角色
*/
public static List getUserRolesByToken(String token) {
String role = (String) getTokenBody(token)
.get(SecurityConstants.ROLE_CLAIMS);
return Arrays.stream(role.split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
}
獲取保存在服務(wù)端的用戶信息類
Spring Security 提供的 UserDetailsService有一個通過名字返回 Spring Security 可用于身份驗證的UserDetails對象的方法:loadUserByUsername()楼雹。
package org.springframework.security.core.userdetails;
/**
*加載用戶特定數(shù)據(jù)的核心接口。
*/
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails包含用于構(gòu)建認(rèn)證對象的必要信息(例如:用戶名尖阔,密碼)贮缅。
package org.springframework.security.core.userdetails;
/**
*提供用戶核心信息的借口
*/
public interface UserDetails extends Serializable {
Collection getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
一般情況下我們需要實現(xiàn) UserDetailsService 借口并重寫其中的 loadUserByUsername() 方法。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
public UserDetailsServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user = userService.findUserByUserName(name);
return new JwtUser(user);
}
}
認(rèn)證過濾器(重要)
第一個過濾器主要JWTAuthenticationFilter用于根據(jù)用戶的用戶名和密碼進(jìn)行登錄驗證(用戶請求中必須有用戶名和密碼這兩個參數(shù))介却,為此我們繼承了 UsernamePasswordAuthenticationFilter 并且重寫了下面三個方法:
attemptAuthentication(): 驗證用戶身份谴供。
successfulAuthentication() : 用戶身份驗證成功后調(diào)用的方法。
unsuccessfulAuthentication(): 用戶身份驗證失敗后調(diào)用的方法齿坷。
/**
* @author shuang.kou
* 如果用戶名和密碼正確桂肌,那么過濾器將創(chuàng)建一個JWT Token 并在HTTP Response 的header中返回它,格式:token: "Bearer +具體token值"
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 設(shè)置登錄請求的 URL
super.setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 從輸入流中獲取到登錄的信息
LoginUser loginUser = objectMapper.readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe());
// 這部分和attemptAuthentication方法中的源碼是一樣的胃夏,
// 只不過由于這個方法源碼的是把用戶名和密碼這些參數(shù)的名字是死的轴或,所以我們重寫了一下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
loginUser.getUsername(), loginUser.getPassword());
return authenticationManager.authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 如果驗證成功,就生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication) {
JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
List roles = jwtUser.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 創(chuàng)建 Token
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), roles, rememberMe.get());
// Http Response Header 中返回 Token
response.setHeader(SecurityConstants.TOKEN_HEADER, token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
這個過濾器中有幾個比較重要的地方說明:
1.UsernamePasswordAuthenticationToken:從登錄請求中獲取{用戶名仰禀,密碼}照雁,AuthenticationManager將使用它來認(rèn)證登錄帳戶。
2.authenticationManager.authenticate(authRequest):這段代碼主要對用戶進(jìn)行認(rèn)證答恶,當(dāng)執(zhí)行這段代碼的時候會跳到UserDetailsServiceImpl中去調(diào)用loadUserByUsername()方法來驗證(我們在配置類中配置了AuthenticationManager使用自定義的UserDetailsServiceImpl去驗證用戶信息)饺蚊。當(dāng)驗證成功后會返回一個完整填充的Authentication對象(包括授予的權(quán)限),然后會去調(diào)用successfulAuthentication方法悬嗓。
package org.springframework.security.authentication;
/**
*嘗試驗證Authentication對象污呼,如果成功,將返回一個完整填充的Authentication對象(包括授予的權(quán)限)包竹。
*/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
授權(quán)過濾器(重要)
這個過濾器繼承了 BasicAuthenticationFilter燕酷,主要用于處理身份認(rèn)證后才能訪問的資源籍凝,它會檢查 HTTP 請求是否存在帶有正確令牌的 Authorization 標(biāo)頭并驗證 token 的有效性。
當(dāng)用戶使用 token 對需要權(quán)限才能訪問的資源進(jìn)行訪問的時候苗缩,這個類是主要用到的饵蒂,下面按照步驟來說一說每一步到底都做了什么。
1.當(dāng)用戶使用系統(tǒng)返回的 token 信息進(jìn)行登錄的時候 酱讶,會首先經(jīng)過doFilterInternal()方法退盯,這個方法會從請求的Header中取出 token 信息,然后判斷 token 信息是否為空以及 token 信息格式是否正確泻肯。
2.如果請求頭中有token 并且 token 的格式正確渊迁,則進(jìn)行解析并判斷 token 的有效性,然后會在 Spring Security 全局設(shè)置授權(quán)信息SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
/**
* 過濾器處理所有HTTP請求灶挟,并檢查是否存在帶有正確令牌的Authorization標(biāo)頭琉朽。例如,如果令牌未過期或簽名密鑰正確膏萧。
*
* @author shuang.kou
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger logger = Logger.getLogger(JWTAuthorizationFilter.class.getName());
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER);
// 如果請求頭中沒有Authorization信息則直接放行了
if (authorization == null || !authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果請求頭中有token漓骚,則進(jìn)行解析,并且設(shè)置授權(quán)信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
super.doFilterInternal(request, response, chain);
}
/**
* 這里從token中獲取用戶信息并新建一個token
*/
private UsernamePasswordAuthenticationToken getAuthentication(String authorization) {
String token = authorization.replace(SecurityConstants.TOKEN_PREFIX, "");
try {
String username = JwtTokenUtils.getUsernameByToken(token);
// 通過 token 獲取用戶具有的角色
List userRolesByToken = JwtTokenUtils.getUserRolesByToken(token);
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, userRolesByToken);
}
} catch (SignatureException | ExpiredJwtException exception) {
logger.warning("Request to parse JWT with invalid signature . Detail : " + exception.getMessage());
}
return null;
}
}
獲取當(dāng)前用戶
我們在講過濾器的時候說過榛泛,當(dāng)認(rèn)證成功的用戶訪問系統(tǒng)的時候蝌蹂,它的認(rèn)證信息會被設(shè)置在 Spring Security 全局中。那么曹锨,既然這樣孤个,我們在其他地方獲取到當(dāng)前登錄用戶的授權(quán)信息也就很簡單了,通過SecurityContextHolder.getContext().getAuthentication();方法即可沛简。
SecurityContextHolder 保存 SecurityContext 的信息齐鲤,SecurityContext 保存已通過認(rèn)證的 Authentication 認(rèn)證信息。
為此椒楣,我們實現(xiàn)了一個專門用來獲取當(dāng)前用戶的類:
/**
* @author shuang.kou
* 獲取當(dāng)前請求的用戶
*/
@Component
public class CurrentUser {
private final UserDetailsServiceImpl userDetailsService;
public CurrentUser(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
public JwtUser getCurrentUser() {
return (JwtUser) userDetailsService.loadUserByUsername(getCurrentUserName());
}
/**
* TODO:由于在JWTAuthorizationFilter這個類注入UserDetailsServiceImpl一致失敗给郊,
* 導(dǎo)致無法正確查找到用戶,所以存入Authentication的Principal為從 token 中取出的當(dāng)前用戶的姓名
*/
private static String getCurrentUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
return (String) authentication.getPrincipal();
}
return null;
}
}
異常相關(guān)
AccessDeniedHandler
JWTAccessDeniedHandler實現(xiàn)了AccessDeniedHandler主要用來解決認(rèn)證過的用戶訪問需要權(quán)限才能訪問的資源時的異常捧灰。
/**
* @author shuang.kou
* AccessDeineHandler 用來解決認(rèn)證過的用戶訪問需要權(quán)限才能訪問的資源時的異常
*/
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
/**
* 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而權(quán)限不足的時候淆九,
* 將調(diào)用此方法發(fā)送401響應(yīng)以及錯誤信息
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
accessDeniedException = new AccessDeniedException("Sorry you don not enough permissions to access it!");
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
AuthenticationEntryPoint
JWTAuthenticationEntryPoint 實現(xiàn)了 AuthenticationEntryPoint 用來解決匿名用戶訪問需要權(quán)限才能訪問的資源時的異常
/**
* @author shuang.kou
* AuthenticationEntryPoint 用來解決匿名用戶訪問需要權(quán)限才能訪問的資源時的異常
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而不提供Token或者Token過期時,
* 將調(diào)用此方法發(fā)送401響應(yīng)以及錯誤信息
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
驗證權(quán)限配置的 Controller
這個是 UserControler 主要用來檢測權(quán)限配置是否生效毛俏。
getAllUser()方法被注解 @PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")修飾代表這個方法可以被DEV炭庙,PM 這兩個角色訪問,而deleteUserById() 被注解 @PreAuthorize("hasAnyRole('ROLE_ADMIN')")修飾代表只能被 ADMIN 訪問煌寇。
/**
* @author shuang.kou
*/
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
private final CurrentUser currentUser;
public UserController(UserService userService, CurrentUser currentUser) {
this.userService = userService;
this.currentUser = currentUser;
}
@GetMapping("/users")
@PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")
public ResponseEntity> getAllUser(@RequestParam(value = "pageNum", defaultValue = "0") int pageNum, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
System.out.println("當(dāng)前訪問該接口的用戶為:" + currentUser.getCurrentUser().toString());
Page allUser = userService.getAllUser(pageNum, pageSize);
return ResponseEntity.ok().body(allUser);
}
@DeleteMapping("/user")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public ResponseEntity deleteUserById(@RequestParam("username") String username) {
userService.deleteUserByUserName(username);
return ResponseEntity.ok().build();
}
}
歡迎大家點贊關(guān)注焕蹄,轉(zhuǎn)發(fā),此外給大家整理了一些詳細(xì)的資料阀溶,私信我“資料”即可獲取