Spring Boot Security 整合 JWT 實現 無狀態的分布式API接口
阿新 • • 發佈:2019-05-16
forbidden 入門教程 eba ava dig arch sso 獲取 == 簡介
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。JSON Web Token 入門教程 - 阮一峰,這篇文章可以幫你了解JWT的概念。本文重點講解Spring Boot 結合 jwt ,來實現前後端分離中,接口的安全調用。
快速上手
之前的文章已經對 Spring Security 進行了講解,這一節對涉及到 Spring Security 的配置不詳細講解。若不了解 Spring Security 先移步到 Spring Boot Security 詳解。
建表
DROP TABLE IF EXISTS `user`; DROP TABLE IF EXISTS `role`; DROP TABLE IF EXISTS `user_role`; DROP TABLE IF EXISTS `role_permission`; DROP TABLE IF EXISTS `permission`; CREATE TABLE `user` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_role` ( `user_id` bigint(11) NOT NULL, `role_id` bigint(11) NOT NULL ); CREATE TABLE `role_permission` ( `role_id` bigint(11) NOT NULL, `permission_id` bigint(11) NOT NULL ); CREATE TABLE `permission` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) NULL, `pid` bigint(11) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO user (id, username, password) VALUES (1,‘user‘,‘e10adc3949ba59abbe56e057f20f883e‘); INSERT INTO user (id, username , password) VALUES (2,‘admin‘,‘e10adc3949ba59abbe56e057f20f883e‘); INSERT INTO role (id, name) VALUES (1,‘USER‘); INSERT INTO role (id, name) VALUES (2,‘ADMIN‘); INSERT INTO permission (id, url, name, pid) VALUES (1,‘/user/hi‘,‘‘,0); INSERT INTO permission (id, url, name, pid) VALUES (2,‘/admin/hi‘,‘‘,0); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
項目結構
resources |___application.yml java |___com | |____gf | | |____SpringbootJwtApplication.java | | |____config | | | |____.DS_Store | | | |____SecurityConfig.java | | | |____MyFilterSecurityInterceptor.java | | | |____MyInvocationSecurityMetadataSourceService.java | | | |____MyAccessDecisionManager.java | | |____entity | | | |____User.java | | | |____RolePermisson.java | | | |____Role.java | | |____mapper | | | |____PermissionMapper.java | | | |____UserMapper.java | | | |____RoleMapper.java | | |____utils | | | |____JwtTokenUtil.java | | |____controller | | | |____AuthController.java | | |____filter | | | |____JwtTokenFilter.java | | |____service | | | |____impl | | | | |____AuthServiceImpl.java | | | | |____UserDetailsServiceImpl.java | | | |____AuthService.java
關鍵代碼
pom.xml
<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>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency>
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-security-jwt?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//校驗用戶
auth.userDetailsService( userDetailsService ).passwordEncoder( new PasswordEncoder() {
//對密碼進行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
//對密碼進行判斷匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
return res;
}
} );
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
//因為使用JWT,所以不需要HttpSession
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//OPTIONS請求全部放行
.antMatchers( HttpMethod.OPTIONS, "/**").permitAll()
//登錄接口放行
.antMatchers("/auth/login").permitAll()
//其他接口全部接受驗證
.anyRequest().authenticated();
//使用自定義的 Token過濾器 驗證請求的Token是否合法
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
}
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
JwtTokenUtil
/**
* JWT 工具類
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final String CLAIM_KEY_USERNAME = "sub";
/**
* 5天(毫秒)
*/
private static final long EXPIRATION_TIME = 432000000;
/**
* JWT密碼
*/
private static final String SECRET = "secret";
/**
* 簽發JWT
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(16);
claims.put( CLAIM_KEY_USERNAME, userDetails.getUsername() );
return Jwts.builder()
.setClaims( claims )
.setExpiration( new Date( Instant.now().toEpochMilli() + EXPIRATION_TIME ) )
.signWith( SignatureAlgorithm.HS512, SECRET )
.compact();
}
/**
* 驗證JWT
*/
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
String username = getUsernameFromToken( token );
return (username.equals( user.getUsername() ) && !isTokenExpired( token ));
}
/**
* 獲取token是否過期
*/
public Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken( token );
return expiration.before( new Date() );
}
/**
* 根據token獲取username
*/
public String getUsernameFromToken(String token) {
String username = getClaimsFromToken( token ).getSubject();
return username;
}
/**
* 獲取token的過期時間
*/
public Date getExpirationDateFromToken(String token) {
Date expiration = getClaimsFromToken( token ).getExpiration();
return expiration;
}
/**
* 解析JWT
*/
private Claims getClaimsFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey( SECRET )
.parseClaimsJws( token )
.getBody();
return claims;
}
}
JwtTokenFilter
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
/**
* 存放Token的Header Key
*/
public static final String HEADER_STRING = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader( HEADER_STRING );
if (null != token) {
String username = jwtTokenUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
AuthServiceImpl
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername( username );
String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
}
關鍵代碼就是這些,其他類代碼參照後面提供的源碼地址。
驗證
登錄,獲取token
curl -X POST -d "username=admin&password=123456" http://127.0.0.1:8080/auth/login
返回
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU1NDQ1MzUwMX0.sglVeqnDGUL9pH1oP3Lh9XrdzJIS42VKBApd2nPJt7e1TKhCEY7AUfIXnzG9vc885_jTq4-h8R6YCtRRJzl8fQ
不帶token訪問資源
curl -X POST -d "name=zhangsan" http://127.0.0.1:8080/admin/hi
返回,拒絕訪問
{
"timestamp": "2019-03-31T08:50:55.894+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/auth/login"
}
攜帶token訪問資源
curl -X POST -H "Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU1NDQ1MzUwMX0.sglVeqnDGUL9pH1oP3Lh9XrdzJIS42VKBApd2nPJt7e1TKhCEY7AUfIXnzG9vc885_jTq4-h8R6YCtRRJzl8fQ" -d "name=zhangsan" http://127.0.0.1:8080/admin/hi
返回正確
hi zhangsan , you have ‘admin‘ role
源碼
https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-jwt
Spring Boot Security 整合 JWT 實現 無狀態的分布式API接口