SpringBoot使用SpringSecurity搭建基於非對稱加密的JWT及前後端分離的搭建
安全問題是一個比較複雜的問題,之前使用過Shiro這個安全框架,確實挺簡單的,後來使用SpringSecurity,SpringSecurity更細粒度可控,現在做專案基本都使用前後端分離的,很少再使用Thymeleaf這類模板引擎,而基於前後端分離的許可權問題,則需要使用JWT(json web token)
本次搭建基於JWT的SpringSecurity,並搭建前後端分離的安全許可權的開發環境,希望讀者有一點springsecurity的基礎
程式碼放在GitHub上
https://github.com/lhc0512/springsecurity-jwt
在pom.xml引入jar包
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId >
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency >
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
寫一個繼承於WebSecurityConfigurerAdapter的配置類,在重寫帶參httpsecurity,注入自定義的各種返回json的Handler
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AjaxLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AjaxAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AjaxAccessDeniedHandler accessDeniedHandler;
@Autowired
private AjaxAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AjaxAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//取消session
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint)
.and()
.authorizeRequests()
.anyRequest()
//使用rbac 角色繫結資源的方式
.access("@rbacauthorityservice.hasPermission(request,authentication)")
//.authenticated()
.and()
//該url比較特殊,需要和login.html的form的action的的url一致
.formLogin().loginPage("/login").successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).permitAll()
.and()
.logout().logoutSuccessHandler(logoutSuccessHandler).permitAll()
.and()
.csrf().disable();
http.rememberMe().rememberMeParameter("remember-me")
.userDetailsService(myUserDetailsService).tokenValiditySeconds(300);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
//使用jwt的Authentication
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 禁用headers快取
http.headers().cacheControl();
}
}
這些handler的寫法基本一樣,你需要先寫一個返回json的類
包含狀態碼,狀態資訊,返回物件,以及token
@Component
public class AjaxResponseBody implements Serializable {
private String status;
private String msg;
private Object result;
private String jwtToken;
以登陸的處理為例,你需要配置好返回的json,使用fastjson進行轉換為json,最後返回給前端
@Component
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();
responseBody.setStatus("00");
responseBody.setMsg("Login Success!");
MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
String jwtToken = JwtTokenUtil.generateToken(myUserDetails.getUsername(), 300);
responseBody.setJwtToken(jwtToken);
response.getWriter().write(JSON.toJSONString(responseBody));
}
}
接下來在springsecurity的核心配置類中新增和資料庫及密碼加密的相關配置,注入自定義userDetailsService
使用BCryptPasswordEncoder進行加密
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception {
//使用資料庫
auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
自定義一個UserDetails類,
@Component
public class MyUserDetails implements UserDetails ,Serializable {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
自定義一個UserDitailsService,為了方便起見,我就不使用mybatis了,在程式碼中模擬從加密的資料庫中查詢使用者資訊,你註冊使用者資訊的時候就該作如下加密
@Component
public class MyUserDetailsService implements UserDetailsService,Serializable {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserDetails myUserDetails = new MyUserDetails();
myUserDetails.setUsername(username);
//模擬從資料庫取出的密碼
myUserDetails.setPassword(new BCryptPasswordEncoder().encode("12345"));
//模擬從資料庫取出的許可權
HashSet<SimpleGrantedAuthority> set = new HashSet<>();
// set.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
set.add(new SimpleGrantedAuthority("ROLE_USER"));
myUserDetails.setAuthorities(set);
return myUserDetails;
}
}
這個BCryptPasswordEncoder很強大,每次加密產生的密碼都不一樣,而認證的使用它又能識別出來,也是現在較為主流的加密演算法,像MD5和SHA256等演算法都被淘汰了
在springsecurity的核心配置有jwtAuthenticationTokenFilter,其配置如下,作用就是把傳過來的token解析為username,再從資料庫中查詢使用者資訊放在authentication中
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
MyUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//請求頭為 Authorization
//請求體為 Bearer token
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
final String authToken = authHeader.substring("Bearer ".length());
String username = JwtTokenUtil.parseToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
注意UsernamePasswordAuthenticationToken的第一個引數有兩種方式,建議傳使用者的整個資訊,因為現在比較流行使用RBAC角色繫結資源的細粒度許可權控制,該方式較為靈活,而不是硬編碼在程式碼中,而使用該方式需要用到使用者的許可權資訊
前面的配置有.access(“@rbacauthorityservice.hasPermission(request,authentication)”)
下面介紹如何使用RBAC
@Component("rbacauthorityservice")
public class RbacAuthorityService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//得到的principal的資訊是使用者名稱還是整個使用者資訊取決於在自定義的authenticationProvider中傳參的方式
Object userInfo = authentication.getPrincipal();
boolean hasPermission = false;
if (userInfo instanceof UserDetails) {
String username = ((UserDetails) userInfo).getUsername();
Collection<? extends GrantedAuthority> authorities = ((UserDetails) userInfo).getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals("ROLE_ADMIN")) {
//admin 可以訪問的資源
Set<String> urls = new HashSet();
urls.add("/sys/**");
urls.add("/test/**");
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
}
//user可以訪問的資源
Set<String> urls = new HashSet();
urls.add("/test/**");
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
} else {
return false;
}
}
}
接下來說說非對稱加密的token怎樣產生和解析的,你可以使用jdk自帶的keytool工具,注意配置好JAVA_HOME,
輸入,如下內容
keytool -genkey -alias jwt -keyalg RSA -keysize 1024 -validity 365 -keystore jwt.jks
意思是使用keytool生成金鑰,別名為jwt,演算法為RSA,有效期為365天,檔名為jwt,jks,把檔案儲存在當前開啟cmd的路徑下,它提示輸入密碼,我就輸入lhc123吧
接下的輸入可以忽略,回車pass
把生成的檔案複製到resources目錄下,寫一個JwtTokenUtil 的生成和解析兩個方法
public class JwtTokenUtil {
//載入jwt.jks檔案
private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.jks");
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;
static {
try {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, "lhc123".toCharArray());
privateKey = (PrivateKey) keyStore.getKey("jwt", "lhc123".toCharArray());
publicKey = keyStore.getCertificate("jwt").getPublicKey();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String generateToken(String subject, int expirationSeconds) {
return Jwts.builder()
.setClaims(null)
.setSubject(subject)
.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
public static String parseToken(String token) {
String subject = null;
try {
Claims claims = Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token).getBody();
subject = claims.getSubject();
} catch (Exception e) {
}
return subject;
}
}
好了專案搭建完畢,內容比較多,我也儘可能減少篇幅,但給大家一個清晰的思路,需要注意的是,每次請求後臺,後臺都需要重新整理token,上名設定的token的有效期是5分鐘,5分鐘不做任何操作就需要重新登入,最標準的做法是把token儲存到redis中,並且設定其有效時間