微服務大行其道的現在,如果我們還在用wsdl之類的提供接口,給人的感覺就會很low,雖然說不能為了炫技而炫技,但是既然restful接口已經越來越流行,必然有它的道理。
本文我們不讨論restful接口的好處,旨在解決使用restful時候的權限控制問題。
springboot本身已經提供了很好的spring security的支援,我們隻需要實作(或者重寫)一部分接口來實作我們的個性化設定即可。本文淺顯易懂,沒有深入原理(後面文章會将,有需要的小夥伴稍等等~~~)。
思路:
1.通過spring security做授權攔截操作
2.通過jwt根據使用者資訊生成token以供後面調用
3.将生成的token放到HttpServletResponse頭資訊中
4.使用的時候從response頭中擷取token放在request頭中送出到背景做認證即可
5.預設逾時時間10天
一、pom
慣例還是先上pom,因為pom可以很直覺的看到本項目用了哪些東西,我這個項目使用了很多的包,這裡貼出了核心的幾個,其他大部分的都會自動引用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
二、登入過濾器
我們采用倒推法,用到什麼找什麼,這也比較符合XP程式設計的思想,不寫多(無)餘(用)的代碼,既然要做認證,很明顯需要一個過濾器來處理所有需要攔截的請求。
UsernamePasswordAuthenticationFilter是security自己提供的過濾器,我們重寫其中的成功方法(successfulAuthentication)來處理我們自己的邏輯,當然根據自己的情況,比如登入失敗處理,重寫(unsuccessfulAuthentication)即可。
1.成功回調中用到一個TokenAuthenticationHandler,即token認證處理類,該類的主要方法就是借用jwt的機制來生成token,以供後面登入授權使用。
2.往response頭資訊中放入參數為“Authorization”,值為“Bearer ”+token的值
package com.mos.eboot.tools.jwt;
import com.mos.eboot.tools.util.FastJsonUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter{
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
private AuthenticationSuccessHandler successHandler;
public JWTLoginFilter() {
}
public JWTLoginFilter(AuthenticationManager authManager) {
setAuthenticationManager(authManager);
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
Object obj = auth.getPrincipal();
if(obj != null) {
UserDetails userDetails = (UserDetails)obj;
String token = tokenAuthenticationHandler.generateToken(FastJsonUtils.toJSONNoConfig(userDetails));
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + token);
}
if(successHandler != null) {
successHandler.onAuthenticationSuccess(req, res, auth);
}
}
public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
}
}
JWTAuthenticationToken
package com.mos.eboot.tools.jwt;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import static java.util.Collections.emptyList;
public class JWTAuthenticationToken extends UsernamePasswordAuthenticationToken{
private static final long serialVersionUID = 1L;
public JWTAuthenticationToken(Object principal) {
super(principal,null,emptyList());
}
@Override
public Object getCredentials() {
return super.getCredentials();
}
@Override
public Object getPrincipal() {
return super.getPrincipal();
}
}
TokenAuthenticationHandler
package com.mos.eboot.tools.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author 小塵哥
*/
public class TokenAuthenticationHandler implements Serializable {
private static final long serialVersionUID = 1L;
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_SUBJECT = "subject";
private static final String DEFAULT_SECRET = "eboot@secret";
private static final Long DEFAULT_EXPIRATION = 864000L;
private String secret = DEFAULT_SECRET;
private Long EXPIRATION = DEFAULT_EXPIRATION;
public TokenAuthenticationHandler() {
}
public String getSubjectFromToken(String token) {
String subject;
try {
final Claims claims = getClaimsFromToken(token);
subject = claims.get(CLAIM_KEY_SUBJECT).toString();
} catch (Exception e) {
subject = null;
}
return subject;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + EXPIRATION * 1000);
}
public String generateToken(String subject) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_SUBJECT, subject);
return generateToken(claims);
}
String generateToken( Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
}
三、認證
從頭資訊中取出Authorization,然後解析出個人資訊,如果個人資訊不為空,則将個人資訊加密後再放入授權域。
package com.mos.eboot.tools.jwt;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class JWTAuthenticationFilter extends GenericFilterBean {
static final String HEADER_STRING = "Authorization";
static final String TOKEN_PREFIX = "Bearer";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
String token = req.getHeader(HEADER_STRING);
if(StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
TokenAuthenticationHandler tokenAuthenticationHandler = new TokenAuthenticationHandler();
String subject = tokenAuthenticationHandler.getSubjectFromToken(token.replace(TOKEN_PREFIX, ""));
if(StringUtils.isNotBlank(subject)) {
SecurityContextHolder.getContext().setAuthentication(new JWTAuthenticationToken(subject));
}
}
filterChain.doFilter(request,response);
}
}
四、調用
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/**").authenticated()
.antMatchers(HttpMethod.POST, "/login").permitAll().anyRequest().permitAll().and()
.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
//注入登入校驗類
@Bean
public JWTLoginFilter loginFilter() throws Exception {
JWTLoginFilter loginFilter = new JWTLoginFilter(authenticationManager());
loginFilter.setSuccessHandler(loginAuthenticationSuccessHandler);
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType("application/json");
response.getWriter().write(FastJsonUtils
.toJSONString(new ResultModel(ResultStatus.FAIL.getCode(), exception.getMessage())));
});
return loginFilter;
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return new UserService();
}
//重寫密碼加密方法
@Bean
public Md5PasswordEncoder passwordEncoder() {
Md5PasswordEncoder passwordEncoder = new Md5PasswordEncoder();
passwordEncoder.setIterations(1);
return passwordEncoder;
}
實作UserDetailsService 接口,定義自己的擷取使用者登入方法實作類
package com.mos.eboot.api.config.support;
import com.mos.eboot.api.platform.api.ISysUserService;
import com.mos.eboot.platform.entity.SysUser;
import com.mos.eboot.tools.result.ResultModel;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author 小塵哥
*/
@Service("userService")
public class UserService implements IUserService {
@Resource
private ISysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return sysUserService.getByUsername(username);
}
}
基本以上就可以搞定基礎的無狀态鑒權了,如果需要更深入的了解,比如權限自定義等,請關注我後面的文章.......