SrpingBoot-Shrio
整合 JWT
(2019.12.23)
JSON Web Token(JWT)
是一個非常輕巧的規範。這個規範允許我們使用JWT
在用戶和服務器之間傳遞安全可靠的信息。
使用基於 Token 的身份驗證方法,在服務端不需要存儲用戶的登錄記錄。
- 客戶端使用用戶名跟密碼請求登錄
- 服務端收到請求,去驗證用戶名與密碼
- 驗證成功後,服務端會簽發一個 Token,再把這個 Token 發送給客戶端
- 客戶端收到 Token 以後可以把它存儲起來,比如放在 Cookie 裏
- 客戶端每次向服務端請求資源的時候需要帶着服務端簽發的 Token
- 服務端收到請求,然後去驗證客戶端請求裏面帶着的 Token,如果驗證成功,就向客戶端返回請求的數據
一個JWT
實際上就是一個字符串,它由三部分組成: 頭部、載荷與簽名。
1. 引入JWT
依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>spring-boot-demo-shiro-base</groupId>
<artifactId>spring-boot-demo-shiro-base</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-demo-shiro6</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis.version>1.3.2</mybatis.version>
<mysql.version>8.0.12</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--使用阿里巴巴的德魯伊作爲數據源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql數據庫-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Shiro 依賴-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0-RC2</version>
</dependency>
<!--jjwt 依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
<!--html頁面使用shiro標籤依賴-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 對象池,使用redis時必須引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
2.application.yml
加上jwt
配置信息
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
#數據源其他配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
redis:
host: localhost
# 連接超時時間(記得添加單位,Duration)
timeout: 10000ms
lettuce:
pool:
# 連接池最大連接數(使用負值表示沒有限制) 默認 8
max-active: 8
# 連接池最大阻塞等待時間(使用負值表示沒有限制) 默認 -1
max-wait: -1ms
# 連接池中的最大空閒連接 默認 8
max-idle: 8
# 連接池中的最小空閒連接 默認 0
min-idle: 0
#jwt 配置
jwt:
config:
key: zhihao #加密密匙
# token有效期,單位秒
jwtTimeOut: 3600
# 後端免認證接口 url
anonUrl: /login,
mybatis:
mapper-locations: mapper/*.xml
type-aliases-package: com.zhihao.entity
3.創建jwtUtil.java
工具類,進行簽發和解析token
@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtil {
@Value("${jwt.config.key}")
private String key;
private long jwtTimeout;//一個小時
private String anonUrl;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getJwtTimeout() {
return jwtTimeout;
}
public void setJwtTimeout(long jwtTimeout) {
this.jwtTimeout = jwtTimeout;
}
public String getAnonUrl() {
return anonUrl;
}
public void setAnonUrl(String anonUrl) {
this.anonUrl = anonUrl;
}
/**
* 生成JWT
*
* @param id 用戶id
* @param subject 用戶名
* @return java.lang.String
*/
public String createJWT(String id, String subject) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setId(id) //id
.setSubject(subject) //主題
.setIssuedAt(now) //簽發時間
.signWith(SignatureAlgorithm.HS256, key); //加密
//超時大於0 設置token超時
if (jwtTimeout > 0) {
//轉換成超時毫秒
long timeout = nowMillis + (jwtTimeout * 1000);
builder.setExpiration(new Date(timeout));
}
return builder.compact();
}
/**
* 解析JWT
*
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
4.自定義自己的過濾器JWTFilter
我們使用的是 shiro
默認的權限攔截 Filter
,而因爲JWT
的整合,我們需要自定義自己的過濾器 JWTFilter,JWTFilter
繼承了 BasicHttpAuthenticationFilter
,並部分原方法進行了重寫。
- 檢驗請求頭是否帶有 token
((HttpServletRequest) request).getHeader("Token") != null
- 如果帶有 token,執行 shiro 的 login() 方法,將 token 提交到 Realm 中進行檢驗;如果沒有 token,說明當前狀態爲遊客狀態(或者其他一些不需要進行認證的接口)
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
private static final String TOKEN = "Token";
private AntPathMatcher pathMatcher = new AntPathMatcher();
private Map errorMap;
@SneakyThrows
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
JwtUtil JwtUtil = context.getBean(JwtUtil.class);
//判斷是否是登錄請求
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String[] split = StringUtils.split(JwtUtil.getAnonUrl(), ",");
for (String url : split) {
//如果是後端免認證接口 直接放行
if (pathMatcher.match(url,httpServletRequest.getRequestURI())){
return true;
}
}
//進入executeLogin方法判斷請求的請求頭是否帶上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,則進入 executeLogin 方法執行登入,檢查 token 是否正確
return executeLogin(request, response);
}else {
this.tokenError(response,"token爲空");
}
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(TOKEN);
try {
// 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
getSubject(request, response).login(new JWTToken(token));
// 如果沒有拋出異常則代表登入成功,返回true
return true;
} catch (Exception e) {
this.tokenError(response, "token認證失敗");
return false;
}
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
return token != null;
}
/**
* token問題響應
*
* @param response
* @param msg
* @return void
* @author: zhihao
* @date: 2019/12/24
* {@link #}
*/
private void tokenError(ServletResponse response,String msg) throws IOException {
errorMap = new LinkedHashMap();
errorMap.put("code", "error");
errorMap.put("msg", msg);
//響應token爲空
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.resetBuffer(); //清空第一次流響應的內容
//轉成json格式
ObjectMapper object = new ObjectMapper();
String asString = object.writeValueAsString(errorMap);
response.getWriter().println(asString);
}
/**
* 對跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發送一個 option請求,這裏我們給 option請求直接返回正常狀態
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
5, JWTToken.java
@Data
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
public JWTToken() {
}
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
}
6. 配置ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 在 Shiro過濾器鏈上加入 JWTFilter
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//所有url都必須認證通過jwt過濾器纔可以訪問
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
// 配置SecurityManager,並注入shiroRealm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//設置緩存管理器 省略..
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
// 配置Realm,需自己實現
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* 開啓shiro認證註解
*
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
7. 配置自己實現的ShiroRealm
public class ShiroRealm extends AuthorizingRealm {
/**
* 支持自定義認證令牌
*
* @param token
* @return boolean
* @author: zhihao
* @date: 2019/12/24
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Autowired
private JwtUtil jwtUtil;
/**
* 授權模塊>>>>獲取用戶角色和權限
*
* @param principal
* @return org.apache.shiro.authz.AuthorizationInfo
* @author: zhihao
* @date: 2019/12/13
* {@link #}
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String username = user.getUsername();
//創建授權對象進行封裝角色和權限信息進去進行返回 注意不是SimpleAuthenticationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//獲取用戶角色集
List<Role> roleList = roleService.findRoleByUserName(username);
Set<String> roleSet = new HashSet<>();
for (Role role : roleList) {
roleSet.add(role.getMemo());
}
System.out.println("用戶擁有的角色>>>"+roleSet);
//添加角色進角色授權
info.setRoles(roleSet);
List<Permission> permissionList = permissionService.findPermissionByUserName(username);
Set<String> permissionSet = new HashSet<>();
for (Permission permission : permissionList) {
permissionSet.add(permission.getName());
}
System.out.println("用戶擁有的權限>>>"+permissionSet);
//添加權限進權限授權
info.setStringPermissions(permissionSet);
return info;
}
/**
* 用戶認證
*
* @param authenticationToken 身份認證 token
* @return AuthenticationInfo 身份認證信息
* @throws AuthenticationException 認證相關異常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
// 這裏的 token是從 JWTFilter 的 executeLogin 方法傳遞過來的
String token = (String) authenticationToken.getCredentials();
String username = null;
try {
username = jwtUtil.parseJWT(token).getSubject();
} catch (Exception e) {
//拋出token認證失敗
throw new AuthenticationException("token認證失敗");
}
// 通過用戶名到數據庫查詢用戶信息
User user = userService.findUserByName(username);
if (user == null) {
throw new UnknownAccountException("用戶不存在!");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("賬號已被鎖定,請聯繫管理員!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName());
return info;
}
}
7. 配置權限不足異常處理器
/**
* @Author: zhihao
* @Date: 2019/12/24 12:57
* @Description: 登錄相關異常處理
* @Versions 1.0
**/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class loginExceptionHandler {
private Logger log = LoggerFactory.getLogger(this.getClass());
private Map exceptionMap;
/**
* 攔截權限不足異常,並響應
*
* @param e
* @return java.util.Map
* @author: zhihao
* @date: 2019/12/24
*/
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseBody
public Map unauthorizedException(UnauthorizedException e){
exceptionMap = new LinkedHashMap();
exceptionMap.put("msg", e.getMessage());
log.error(e.getMessage());
return exceptionMap;
}
}
8.登錄接口,校驗成功簽發token
/**
* 登錄免認證 登錄成功簽發token
*
* @param username 用戶名
* @param password 密碼
* @return java.util.Map 簡陋的結果包裝
* @author: zhihao
* @date: 2019/12/12
*/
@PostMapping("/login")
public Map login(@RequestParam("username") String username,@RequestParam("password") String password){
resultMap = new LinkedHashMap<>();
// 密碼加密
String md5 = new SimpleHash("MD5", password, username, 1024).toString();
User user = userService.findUserByName(username);
if(user != null && md5.equals(user.getPassword())){
resultMap.put("code", "success");
resultMap.put("token", jwtUtil.createJWT(user.getId(), user.getUsername()));
return resultMap;
}
resultMap.put("code","error");
resultMap.put("msg","用戶不存在或者密碼錯誤");
return resultMap;
}