一 前言
大家好,我是知識追尋者,本篇內容是springSecurity第四篇;沒有相關基礎的同學請學習后再來看這篇內容;文末附源碼地址;
二 pom
pom 文件引入的依賴 , security 的啟動器支持security 功能;lombok 進行簡化開發; fastjson 進行Json處理;
jjwt 進行jwt token 支持;lang3 字符串處理;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
</dependencies>
三 認證流程
SecurityContextHolder
,提供SecurityContext
的訪問權限。SecurityContext
,保存Authentication
和可能的特定於請求的安全信息。Authentication
,以特定於Spring Security的方式代表校驗。GrantedAuthority
,以反映授予主體的應用程序范圍的權限。UserDetails
,提供從應用程序的DAO或其他安全數據源構建Authentication對象所需的信息。UserDetailsService
,在基於String
的用戶名(或證書ID等)中傳遞時創建UserDetails
。
上面的意思不難理解, 從數據源中獲取 用戶信息 組裝到 UserDetails
, 然后通過UserDetailsService
,傳遞 UserDetails
; SecurityContextHolder
存儲 整個 用戶上下文信息,通過SecurityContext
存儲 Authentication
, 這樣就保證了 springSecurity 持有用戶信息;
四 實體
SysUser 實現 UserDetails 用於儲存用戶信息, 主要是用戶名,密碼, 和權限;
/**
* @Author lsc
* <p> </p>
*/
@Data
public class SysUser implements UserDetails {
// 用戶名
private String username;
// 密碼
private String password;
// 權限信息
private Set<? extends GrantedAuthority> 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;
}
}
五 token工具類
token 工具類主要用於生產 token, 解析token, 校驗token;這邊需要注意的是,將 權限 歸並到了生成 toekn 的步驟,這樣通過 token就可以獲取 權限,在權限校驗時通過token就可以獲取權限信息;缺點就進行授權的之后的token應為未更新會造成權限未同步;
/**
* @Author lsc
* <p> </p>
*/
public class JwtUtil {
private static final String CLAIMS_ROLE = "zszxzRoles";
/**
* 5天(毫秒)
*/
private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 5;
/**
* JWT密碼
*/
private static final String SECRET = "secret";
/**
* 簽發JWT
*/
public static String getToken(String username, String roles) {
Map<String, Object> claims = new HashMap<>(8);
// 主體
claims.put( CLAIMS_ROLE, roles);
return Jwts.builder()
.setClaims(claims)
.claim("username",username)
.setExpiration( new Date( Instant.now().toEpochMilli() + EXPIRATION_TIME ) )// 過期時間
.signWith( SignatureAlgorithm.HS512, SECRET )// 加密
.compact();
}
/**
* 驗證JWT
*/
public static Boolean validateToken(String token) {
return (!isTokenExpired( token ));
}
/**
* 獲取token是否過期
*/
public static Boolean isTokenExpired(String token) {
Date expiration = getExpireTime( token );
return expiration.before( new Date() );
}
/**
* 根據token獲取username
*/
public static String getUsernameByToken(String token) {
String username = (String) parseToken( token ).get("username");
return username;
}
public static Set<GrantedAuthority> getRolseByToken(String token) {
String rolse = (String) parseToken( token ).get(CLAIMS_ROLE);
String[] strArray = StringUtils.strip(rolse, "[]").split(", ");
Set<GrantedAuthority> authoritiesSet = new HashSet();
if (strArray.length>0){
Arrays.stream(strArray).forEach(rols-> {
GrantedAuthority authority = new SimpleGrantedAuthority(rols);
authoritiesSet.add(authority);
});
}
return authoritiesSet;
}
/**
* 獲取token的過期時間
*/
public static Date getExpireTime(String token) {
Date expiration = parseToken( token ).getExpiration();
return expiration;
}
/**
* 解析JWT
*/
private static Claims parseToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey( SECRET )
.parseClaimsJws( token )
.getBody();
return claims;
}
}
六 UserDetailsService
UserDetailsService 用戶查詢數據庫的數據信息,進行用戶數據封裝到UserDetails, 在進行用戶身份認證的時候會走這邊; 這邊采用官方提供的PasswordEncoder 進行加密; 其配置方式需要在WebSecurityConfig 中 配置;
/**
* @Author lsc
* <p> </p>
*/
@Component
@Slf4j
public class SysUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
// 登陸驗證時,通過username獲取用戶的所有權限信息; 正式環境中就是查詢用戶數據授權
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("------用戶{}身份認證-----",username);
// 新建用戶
SysUser user = new SysUser();
// 賬號
user.setUsername(username);
// 密碼
user.setPassword(passwordEncoder.encode("123456"));
// 設置權限
Set authoritiesSet = new HashSet();
// 注意角色權限需要加 ROLE_前綴,否則報403
GrantedAuthority userPower = new SimpleGrantedAuthority("ROLE_USER");
GrantedAuthority adminPower = new SimpleGrantedAuthority("ROLE_ADMIN");
authoritiesSet.add(userPower);
authoritiesSet.add(adminPower);
user.setAuthorities(authoritiesSet);
return user;
}
}
七 JWTLoginFilter
JWTLoginFilter 繼承 AbstractAuthenticationProcessingFilter 過濾器;理論上繼承 UsernamePasswordAuthenticationFilter 也是 可行,畢竟 UsernamePasswordAuthenticationFilter 是 AbstractAuthenticationProcessingFilter 的實現類;
JWTLoginFilter 用於用戶登陸認證,其實現如下 三個方法 ;
- attemptAuthentication 用於 嘗試認證,如果認證成功會走 successfulAuthentication 方法;如果認證失敗會走 unsuccessfulAuthentication 方法;
- successfulAuthentication 認證成功后我們需要生成一個token,返回以JSON的形式返回給前端;
- unsuccessfulAuthentication 認證失敗,我們通過異常信息判定,然后返回錯誤信息給前端;
/**
* @Author lsc
* <p> 登陸認證過濾器 </p>
*/
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
/**
* @Author lsc
* <p> 登陸認證</p>
* @Param [request, response]
* @Return
*/
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword());
return getAuthenticationManager().authenticate(authenticationToken);
}
/**
* @Author lsc
* <p> 登陸成功返回token</p>
* @Param [request, res, chain, auth]
* @Return
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication auth){
SysUser principal = (SysUser)auth.getPrincipal();
String token = JwtUtil.getToken(principal.getUsername(),principal.getAuthorities().toString());
try {
//登錄成功時,返回json格式進行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,token);
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String result="";
// 賬號過期
if (failed instanceof AccountExpiredException) {
result="賬號過期";
}
// 密碼錯誤
else if (failed instanceof BadCredentialsException) {
result="密碼錯誤";
}
// 密碼過期
else if (failed instanceof CredentialsExpiredException) {
result="密碼過期";
}
// 賬號不可用
else if (failed instanceof DisabledException) {
result="賬號不可用";
}
//賬號鎖定
else if (failed instanceof LockedException) {
result="賬號鎖定";
}
// 用戶不存在
else if (failed instanceof InternalAuthenticationServiceException) {
result="用戶不存在";
}
// 其他錯誤
else{
result="未知異常";
}
// 處理編碼方式 防止中文亂碼
response.setContentType("text/json;charset=utf-8");
// 將反饋塞到HttpServletResponse中返回給前台
response.getWriter().write(JSON.toJSONString(result));
}
}
八 WebSecurityConfig
WebSecurityConfig 是 springSecurity 的配置相關信息;在配置中,可以進行數據訪問權限限制,授權異常處理,賬號加密方式等配置;
/**
* @Author lsc
* <p> </p>
*/
@EnableWebSecurity// 開啟springSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DenyHandler denyHandler;
@Autowired
OutSuccessHandler outSuccessHandler;
@Autowired
SysUserDetailsService userDetailsService;
@Autowired
ExpAuthenticationEntryPoint expAuthenticationEntryPoint;
/* *
* @Author lsc
* <p> 授權</p>
* @Param [http]
*/
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()// 授權
.antMatchers("/api/download/**").anonymous()// 匿名用戶權限
.antMatchers("/api/**").hasRole("USER")//普通用戶權限
.antMatchers("/api/admin/**").hasRole("ADMIN")// 管理員權限
.antMatchers("/login").permitAll()
//其他的需要授權后訪問
.anyRequest().authenticated()
.and()// 異常
.exceptionHandling()
.accessDeniedHandler(denyHandler)//授權異常處理
.authenticationEntryPoint(expAuthenticationEntryPoint)// 認證異常處理
.and()
.logout()
.logoutSuccessHandler(outSuccessHandler)
.and()
.addFilterBefore(new JWTLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
// 設置Session的創建策略為:Spring Security不創建HttpSession
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable();// 關閉 csrf 否則post
}
/* *
* @Author lsc
* <p>認證 設置加密方式 </p>
* @Param [auth]
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
九 Handler
配置中使用到了3個處理類,分別是 denyHandler, outSuccessHandler, expAuthenticationEntryPoint;
其中 denyHandler 當權限進行校驗時,如果權限不足就會走這個處理類
/**
* @Author lsc
* <p> 權限不足處理 </p>
*/
@Component
public class DenyHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 設置響應頭
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.error(CodeMsg.PERM_ERROR);
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
outSuccessHandler 是退出登陸處理類,默認地址 localhost:8080/logout;
/**
* @Author lsc
* <p> </p>
*/
@Component
public class OutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 設置響應頭
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,"退出登陸成功");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
expAuthenticationEntryPoint 負責身份認證通過后異常處理,每個主要身份驗證系統都有自己的AuthenticationEntryPoint
實現;
/**
* @Author lsc
* <p> </p>
*/
@Component
public class ExpAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 設置響應頭
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.error(CodeMsg.ACCOUNT_ERROR);
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
十 Controller
SysUserController 用於 提供權限測試
/**
* @Author lsc
* <p> </p>
*/
@RestController
public class SysUserController {
@GetMapping("api/admin")
@PreAuthorize("hasAuthority('ADMIN')")
public String authAdmin() {
return "需要ADMIN權限";
}
@GetMapping("api/test")
@PreAuthorize("hasAuthority('USER')")
public String authUser() {
return "需要USER權限";
}
}
整體項目結構如下
十一 測試
用戶登陸 ,返回token
請求接口測試,返回數據
用戶退出返回信息;
最后
參考文檔
https://blog.csdn.net/Piconjo/article/details/106156383
https://www.jianshu.com/p/8bd4a6e27e7f
https://www.jianshu.com/p/bd882078fac4
https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/
源碼地址:歡迎關注公眾號:知識追尋者 回復 springSecurity 即可獲取啦