前言
我的項目是用SpringBoot 搭建的一個App-Server,用來響應移動端的訪問請求,設計的方式是前後端分離的 。本來對權限的做法是在請求裏面加上token 字段,然後服務器端再對token做解析,得到userid,再根據userid 查找數據庫,來判斷當前用戶是否有權限訪問這個接口。token 是用的JWT;這樣做除了每個接口都要寫解析token 和 權限的判斷代碼外,感覺也沒有其他問題。
哪位有經驗的兄弟能解答一下這樣做有什麼不妥的地方嗎?不甚感激。測試接口和服務端代碼如下:
@PostMapping("/test")
@ResponseBody
public BaseResponse test(@RequestBody BaseQueryDataReq req) {
BaseResponse rsp = new BaseResponse();
if(req.getToken() == null || req.getToken().isEmpty())
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
else if(JwtHelper.isTokenExpiration(req.getToken()))
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_EXPIRE);
String userid = JwtHelper.getUserIdFromToken(req.getToken());
if(userid == null || userid.isEmpty() || !req.getUserid().equals(userid))
return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
...
...
}
這個方法是參考各大開發者平臺的接口定義來的;他們的接口中,大部分都需要在json裏面加上一個token字段。但看了Spring Security的一些文章後,覺得不用這個就感覺不正宗一樣,所以我也嘗試着研究Spring Security。
基礎概念
RESTful API認證方式
一般來講,對於RESTful API都會有認證(Authentication)和授權(Authorization)過程,保證API的安全性。
Authentication vs. Authorization
Authentication指的是確定這個用戶的身份(用戶賬號),Authorization是確定該用戶擁有什麼操作權限,(用戶角色role)。
認證方式一般有三種
-
Basic Authentication
這種方式是直接將用戶名和密碼放到Header中,使用Authorization: Basic Zm9vOmJhcg==
,使用最簡單但是最不安全。
- TOKEN認證
這種方式也是再HTTP頭中,使用Authorization: Bearer <token>
,使用最廣泛的TOKEN是JWT,通過簽名過的TOKEN。
- OAuth2.0
這種方式安全等級最高,但是也是最複雜的。如果不是大型API平臺或者需要給第三方APP使用的,沒必要整這麼複雜。
一般項目中的RESTful API使用JWT來做認證就足夠了。
spring security認證的實現方式:
實現方式大致可以分爲這幾種:
1.配置文件實現,只需要在配置文件中指定攔截的url所需要權限、配置userDetailsService指定用戶名、密碼、對應權限,就可以實現。
2.實現UserDetailsService,loadUserByUsername(String userName)方法,根據userName來實現自己的業務邏輯返回UserDetails的實現類,需要自定義User類實現UserDetails,比較重要的方法是getAuthorities(),用來返回該用戶所擁有的權限。
3.通過自定義filter重寫spring security攔截器,實現動態過濾用戶權限。
4.通過自定義filter重寫spring security攔截器,實現自定義參數來檢驗用戶,並且過濾權限。
我要講的就是第二種方式,用自定義的User類來實現UserDetails,UserDetails的源碼如下, 從源碼可以看出,我們需要重點實現的是獲取用戶權限,用戶名,用戶密碼這三個接口;簡單實現見JwtUserDetails;
public interface UserDetails extends Serializable {
// ~ Methods
// ========================================================================================================
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
*
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}
public class JwtUserDetails implements UserDetails {
private String userName;
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
爲什麼要搞這麼一個UserDetails 呢,用來幹嘛的呢?這個沒有太明白,估計得看源碼才能徹底搞明白裏面的流程。
Spring Security中進行身份驗證的是AuthenticationManager接口,ProviderManager是它的一個默認實現,但它並不用來處理身份認證,而是委託給配置好的AuthenticationProvider,每個AuthenticationProvider會輪流檢查身份認證。檢查後或者返回Authentication對象或者拋出異常。
驗證身份就是加載相應的UserDetails,看看是否和用戶輸入的賬號、密碼、權限等信息匹配。此步驟由實現AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService驗證用戶名、密碼和授權)處理。包含 GrantedAuthority 的 UserDetails對象在構建 Authentication對象時填入數據。
最終我需要實現的就是:
1,在login 接口中 返回JWT 的token,其中攜帶username 信息;
2,在Spring Security框架中,自定義一個filter ,在 UserNamePasswordFilter 之前進行驗證,驗證通過後,寫入SpringSecurityContext;
3,除獲取token 之外的接口(註冊,登錄)其他接口需要攜帶token 訪問;
配置文件如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtUserDetailService jwtUserDetailService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 設置UserDetailsService
.userDetailsService(jwtUserDetailService)
// 使用BCrypt進行密碼的hash
.passwordEncoder(passwordEncoder());
}
// 裝載BCrypt密碼編碼器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailService);
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/auth").authenticated() // 需攜帶有效 token
.antMatchers("/admin").hasAuthority("admin") // 需擁有 admin 這個權限
.antMatchers("/ADMIN").hasRole("ADMIN") // 需擁有 ADMIN 這個身份
.antMatchers("/register").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated() // 允許所有請求通過
.and()
.csrf()
.disable() // 禁用 Spring Security 自帶的跨域處理
.sessionManagement() // 定製我們自己的 session 策略
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 調整爲讓 Spring Security 不創建和使用 session
httpSecurity.addFilterBefore(authenticationTokenFilterBean(),JwtTokenFilter.class);
}
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
JwtTokenFilter authenticationTokenFilter = new JwtTokenFilter(authenticationManagerBean());
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationTokenFilter;
}
filter 如下:
@Component
public class JwtTokenFilter extends UsernamePasswordAuthenticationFilter {
/**
* json web token 在請求頭的名字
*/
@Value("${token.header}")
private String tokenHeader;
/**
* 輔助操作 token 的工具類
*/
@Autowired
private JwtTokenUtils tokenUtils;
@Autowired
private JwtUserDetailService userDetailsService;
@Autowired
private JwtTokenUtils jwtTokenUtil;
public JwtTokenFilter(AuthenticationManager authenticationManager) {
setAuthenticationManager(authenticationManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 將 ServletRequest 轉換爲 HttpServletRequest 才能拿到請求頭中的 token
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 嘗試獲取請求頭的 token
String authToken = httpRequest.getHeader(this.tokenHeader);
System.out.println("getHeader(\"Authorization\")" + httpRequest.getHeader("Authorization"));
// 嘗試拿 token 中的 username
// 若是沒有 token 或者拿 username 時出現異常,那麼 username 爲 null
String username = this.tokenUtils.getUsernameFromToken(authToken);
// 如果上面解析 token 成功並且拿到了 username 並且本次會話的權限還未被寫入
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (this.tokenUtils.validateToken(authToken, userDetails)) {
// 生成通過認證
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
// 將權限寫入本次會話
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
API 接口如下: login ; register 接口不需要token可以訪問; query_test 接口需要在header 裏攜帶token
@RestController
public class MainControler {
@Autowired
UserRepository userRepository;
@Autowired
JwtTokenUtils tokenUtils;
@PostMapping("/login")
@ResponseBody
public BaseReq login(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
BaseReq rsp = new BaseReq();
String token = tokenUtils.generateToken(req.getUsername());
rsp.setToken(token);
return rsp;
}
@PostMapping("/register")
@ResponseBody
public AppUser register(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
if(u == null)
u = userRepository.save(req);
else
u = userRepository.save(u);
String token = tokenUtils.generateToken(req.getUsername());
System.out.println("token : "+ token);
return u;
}
@PostMapping("/query_test")
@ResponseBody
public AppUser query(@RequestBody AppUser req)
{
AppUser u = userRepository.findByUsername(req.getUsername());
return u;
}
不帶token 訪問時,提示403 ,無權限;攜帶token 後,能正常訪問。
https://blog.csdn.net/my_learning_road/article/details/79833802