文章首發(fā)于微信公眾號《程序員果果》
地址:https://mp.weixin.qq.com/s/QO5L1-RCR-jmIuHlZIfkqQ
本篇源碼:https://github.com/gf-huanchupk/SpringBootLearning
簡介
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案娇钱。JSON Web Token 入門教程 - 阮一峰煌珊,這篇文章可以幫你了解JWT的概念冲茸。本文重點講解Spring Boot 結合 jwt ,來實現(xiàn)前后端分離中宪巨,接口的安全調(diào)用。
快速上手
之前的文章已經(jīng)對 Spring Security 進行了講解溜畅,這一節(jié)對涉及到 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";
/**
* 簽發(fā)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() );
}
/**
* 根據(jù)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 系列精選
Spring Boot 自定義 starter
Spring Boot 整合 mybatis-plus
Spring Boot 整合 spring cache
Spring Boot 整合 rabbitmq
Spring Boot 整合 elasticsearch
Spring Boot 整合 docker
Spring Boot 整合 elk
Spring Boot Admin 2.0 詳解
Spring Boot 整合 apollo
Spring Boot Security 詳解
Spring Boot Security 整合 OAuth2 設計安全API接口服務
Spring Boot Security 整合 JWT 實現(xiàn) 無狀態(tài)的分布式API接口
Spring Cloud 系列精選
Spring Cloud Gateway 入門
Spring Cloud Gateway 之 Predict
Spring Cloud Gateway 之 Filter
Spring Cloud Gateway 之 限流
Spring Cloud Gateway 之 服務注冊與發(fā)現(xiàn)
Dubbo 系列精選
Dubbo 搭建管理控制臺
Dubbo 創(chuàng)建 提供者 消費者
Dubbo 配置
Dubbo 高可用
Docker 系列精選
服務 Docker 化
Docker 私有倉庫搭建
DockerSwarm 集群環(huán)境搭建
DockerSwarm 微服務部署