最近在作畢業設計,涉及到微信小程序的開發,要求前端小程序用戶使用微信身份登陸,登錄成功後,後臺返回自定義登陸狀態token給小程序,後續小程序發送API請求都須要攜帶token才能訪問後臺數據。html
本文是對接微信小程序,實現自定義登陸狀態的一個完整示例,實現了小程序的自定義登錄,將自定義登錄態token返回給小程序做爲登錄憑證。用戶的信息保存在數據庫中,登錄態token緩存在redis中。涉及的技術棧:前端
項目GitHub地址:https://github.com/gongsir0630/shiro-jwt-demojava
基礎知識預備:mysql
其餘說明:git
本文只對shiro和jwt整合進行介紹說明,具體的微信登陸實現是使用RestTemplate
調用我本身的wx-java-miniapp
項目,該項目基於WxJava
實現,支持多個小程序登陸、消息處理。github
本文使用如下調用處理便可:web
// 1. todo: 微信登陸: code + appid -> openId + session_key
// appid: 從配置文件讀取
MultiValueMap<String, Object> request = new LinkedMultiValueMap<>();
// 參數封裝, 微信登陸須要如下參數
request.add("code", code);
// eg: http://localhost:8081/wx/user/{appid}/login
String path = url+"/user/"+appid+"/login";
// 請求
JSONObject dto = restTemplate.postForObject(path, request, JSONObject.class);
log.info("--->>>來自[{}]的返回 = [{}]",path,dto);
// 2. todo: 使用openId和session_key生成自定義登陸狀態 -> token
項目地址:redis
先了解一下小程序官方登陸流程,官方說明戳這裏算法
小程序登陸流程spring
wx.login()
獲得code
,將code發送到後臺,後臺經過wx-java-miniapp
獲取到用戶的openId
和session_key
;token
,而且後臺在數據庫中查詢openId
判斷是否存在,根據查詢結果封裝不一樣的消息,最後連同token
一塊兒返回給小程序;header
中添加Authorization
字段,後臺會進行token
的校驗,若是有誤會直接返回401
。uuid
隨機生成一個jwt-idopenId
、session_key
連同jwt-id
一塊兒,使用小程序的appid
進行簽名加密並設置過時時間,最終生成token
"JWT-SESSION-"+jwt-id
和token
以key-value的形式存入redis
中,並設置相同的過時時間"JWT-SESSION-"+jwt-id
爲key從redis中獲取redisTokenredisToken
的攜帶信息,從新以相同的方式生成驗證器,同token
進行校驗比對MySQL
做爲做爲主庫,若是是clone
的項目,請在運行以前準備好相應的數據庫,並修改配置信息。redis
,使用默認配置便可,無需修改。lombok
簡化開發,請在idea或者eclipse安裝lombok插件。新建一個SpringBoot項目,修改pom文件,添加相關dependency:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.gongsir0630</groupId>
<artifactId>shiro-jwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-jwt-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!-- shiro: 用戶認證\接口鑑權 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- jwt: token認證 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<!-- redis: 數據緩存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- druid數據庫鏈接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- mybatis-plus: 操做數據庫 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- mysql驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 工具: 簡化model開發 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 單元測試工具 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.github.gongsir0630.shirodemo.ShiroJwtDemoApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
注意JDK版本:1.8
配置你的application.yml ,主要是配置你的小程序appid和url,還有你的數據庫和redis。
# 設置日誌級別
logging:
level:
org.springframework.web: info
com.github.gongsir0630.shirodemo: debug
# dev環境配置文件
spring:
# 數據庫相關配置信息: 無需再本地安裝mysql,使用yzhelp.top雲端數據庫
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro-jwt-demo
username: username
password: password
# redis 配置信息: 在本地安裝redis
redis:
host: 127.0.0.1
port: 6379
database: 0
---
# 服務啓動的端口號
server:
port: 8080
---
# 微信小程序配置 appid / url
wx:
# 小程序AppId
appid: appid
# 自研小程序接口調用地址
url: http://localhost:8081/wx
說明:
appid: 當前小程序的appid
url: wx-java-miniapp項目接口地址
在啓動類中配置fastJson
-> ShiroJwtDemoApplication.java
/**
* @author gongsir <a href="https://github.com/gongsir0630">碼之淚殤</a>
* 描述: Spring Boot 工程啓動類,能夠直接點擊下面的main方法運行程序
*/
@SpringBootApplication
public class ShiroJwtDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroJwtDemoApplication.class, args);
}
/**
* fastjson 配置注入: 使用阿里巴巴的 fastjson 處理 json 信息
* @return HttpMessageConverters
*/
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 消息轉換對象
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// fastjson 配置
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.PrettyFormat);
config.setDateFormat("yyyy-MM-dd");
// 配置注入消息轉換器
converter.setFastJsonConfig(config);
// 讓 spring 使用自定義的消息轉換器
return new HttpMessageConverters(converter);
}
}
配置Redis -> RedisConfig.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 14:15
* 你的指尖,擁有改變世界的力量
* 描述: Redis配置
* EnableCaching: 開啓緩存
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.create(factory);
}
}
配置RestTemplate -> RestTemplateConfig.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 14:10
* 你的指尖,擁有改變世界的力量
* 描述: RestTemplate的配置類
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// 鏈接超時時間設置爲10秒
factory.setConnectTimeout(1000 * 10);
// 讀取超時時間爲單位爲60秒
factory.setReadTimeout(1000 * 60);
return factory;
}
}
CodeMsg.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 20:17
* 你的指尖,擁有改變世界的力量
* 描述: code和msg封裝
*/
public class CodeMsg {
private final int code;
private final String msg;
public static CodeMsg SUCCESS=new CodeMsg(0,"success");
public static CodeMsg LOGIN_FAIL = new CodeMsg(-1,"code2session failure, please try aging");
public static CodeMsg NO_USER = new CodeMsg(1000,"user not found");
public static CodeMsg SESSION_KEY_ERROR = new CodeMsg(1001,"sessionKey is invalid");
public static CodeMsg TOKEN_ERROR = new CodeMsg(1002,"token is invalid");
public static CodeMsg SHIRO_ERROR = new CodeMsg(1003,"token is invalid");
public CodeMsg(int code, String msg) {
this.code=code;
this.msg=msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
@Override
public String toString() {
return "CodeMsg{" +
"code=" + code +
", msg='" + msg + '\'' +
'}';
}
}
Result.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 18:45
* 你的指尖,擁有改變世界的力量
* 描述:
* 輸出結果的封裝
* 只要get不要set,進行更好的封裝
* @param <T> data泛型
*/
public class Result<T> {
private int code;
private String msg;
private T data;
private Result(T data){
this.code=0;
this.msg="success";
this.data=data;
}
private Result(CodeMsg mg, T data) {
if (mg==null){
return;
}
this.code=mg.getCode();
this.msg=mg.getMsg();
this.data=data;
}
/**
* 成功時
* @param <T> data泛型
* @return Result
*/
public static <T> Result<T> success(T data){
return new Result<T>(data);
}
/**
* 失敗
* @param <T> data泛型
* @return Result
*/
public static <T> Result<T> fail(CodeMsg mg, T data){
return new Result<T>(mg,data);
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
}
自定義異常 -> ApiAuthException.java
import com.github.gongsir0630.shirodemo.controller.res.CodeMsg;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 20:24
* 你的指尖,擁有改變世界的力量
* 描述: 自定義異常, 用於處理Api認證失敗異常信息保存
*/
public class ApiAuthException extends RuntimeException {
private CodeMsg codeMsg;
public ApiAuthException() {
super();
}
public ApiAuthException(CodeMsg codeMsg) {
super(codeMsg.getMsg());
this.codeMsg = codeMsg;
}
public CodeMsg getCodeMsg() {
return codeMsg;
}
public void setCodeMsg(CodeMsg codeMsg) {
this.codeMsg = codeMsg;
}
}
全局異常處理 -> AppExceptionHandler.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 15:49
* 你的指尖,擁有改變世界的力量
* 描述: 全局異常處理
*/
@RestControllerAdvice
@Slf4j
public class AppExceptionHandler {
/**
* 處理 Shiro 異常
* @param e 異常信息
* @return json
*/
@ExceptionHandler({ShiroException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<Result<JSONObject>> handShiroException(ShiroException e) {
log.error("--->>> 捕捉到 [ApiAuthException] 異常: {}", e.getMessage());
return new ResponseEntity<>(Result.fail(CodeMsg.SHIRO_ERROR,null), HttpStatus.UNAUTHORIZED);
}
/**
* 處理 自定義ApiAuthException異常
* @param e 異常信息
* @return json
*/
@ExceptionHandler({ApiAuthException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<Result<JSONObject>> handApiAuthException(ApiAuthException e) {
log.error("--->>> 捕捉到 [ApiAuthException] 異常: {},{}",e.getCodeMsg().getCode(),e.getCodeMsg().getMsg() );
return new ResponseEntity<>(Result.fail(e.getCodeMsg(),null), HttpStatus.UNAUTHORIZED);
}
}
注意:這裏是業務數據庫,也就是咱們小程序用戶信息都由咱們本身存儲,第一次默認使用微信公開信息註冊,以後用戶能夠自行更新這些信息,和微信信息獨立開。
建立對應的實體類 -> User.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/21 23:44
* 你的指尖,擁有改變世界的力量
* 描述: 業務用戶信息
*/
@Data
@TableName("user")
public class User {
/**
* 主鍵,數據庫字段爲user_id -> userId == openId
*/
@TableId(value = "user_id",type = IdType.INPUT)
private String userId;
private String name;
private String photo;
private String sex;
private String grade;
private String college;
private String contact;
}
使用MyBatis-plus建立mapper接口 -> UserMapper.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 19:44
* 你的指尖,擁有改變世界的力量
* 描述: User類mapper接口,繼承自BaseMapper(已經實現User的CRUD)
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
MyBatis-Plus配置 -> MybatisPlusConfig.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/19 17:15
* 你的指尖,擁有改變世界的力量
* 描述: MyBatis-Plus插件配置
*/
@Configuration
@MapperScan("com.github.gongsir0630.shirodemo.mapper")
public class MybatisPlusConfig {
}
建立User業務接口,這裏僅僅演示login -> UserService.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 19:49
* 你的指尖,擁有改變世界的力量
* 描述: 用戶接口
*/
public interface UserService extends IService<User> {
/**
* 登陸
* @param jsCode 小程序code
* @return 登陸信息: 包含token
*/
Map<String, String> login(String jsCode);
}
再建立一個微信登陸信息對象,主要用做接收微信的openid和session_key,以及用做shiro認證 -> WxAccount.java
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 19:58
* 你的指尖,擁有改變世界的力量
* 描述: 微信認證信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class WxAccount {
private String openId;
private String sessionKey;
}
注意:該類不會用於業務信息交互,因此不須要Mapper與db交互。
微信登陸接口,在這裏實現與微信服務器的信息交互 -> WxAccountService.java
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/22 20:06
* 你的指尖,擁有改變世界的力量
* 描述: 微信接口
*/
public interface WxAccountService {
/**
* 微信小程序用戶登錄,完整流程可參考下面官方地址,本例中是按此流程開發
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
* 1 . 微信小程序端傳入code。
* 2 . 經過wx-java-miniapp項目調用微信code2session接口獲取openid和session_key
*
* @param code 小程序端 調用 wx.login 獲取到的code,用於調用 微信code2session接口
* @return JSONObject: 包含openId和sessionKey
*/
WxAccount login(String code);
}
接口實現邏輯:
appid
和url
;path
,例如登陸是 {url}/wx/user/{appid}/login
;code
;RestTemplate
發起登陸請求;代碼實現 -> WxAccountServiceImpl.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 16:12
* 你的指尖,擁有改變世界的力量
* 描述: 微信接口實現: 用 restTemplate 調用 [wxApp] 應用的接口
*/
@Service
@Slf4j
public class WxAccountServiceImpl implements WxAccountService {
@Value("${wx.appid}")
private String appid;
@Value("${wx.url}")
private String url;
@Resource
private RestTemplate restTemplate;
@Override
public WxAccount login(String code) {
// todo: 微信登陸: code + appid -> openId + session_key
// appid: 從配置文件讀取
MultiValueMap<String, Object> request = new LinkedMultiValueMap<>();
// 參數封裝, 微信登陸須要如下參數
request.add("code", code);
// eg: http://localhost:8081/wx/user/{appid}/login
String path = url+"/user/"+appid+"/login";
// 請求
JSONObject dto = restTemplate.postForObject(path, request, JSONObject.class);
log.info("--->>>來自[{}]的返回 = [{}]",path,dto);
int errCode = -1;
if (dto != null ) {
errCode = Integer.parseInt(dto.get("code").toString());
} else {
throw new ApiAuthException(CodeMsg.LOGIN_FAIL);
}
if (0 != errCode) {
throw new ApiAuthException(new CodeMsg(Integer.parseInt(dto.get("code").toString()),
dto.get("msg").toString()));
}
// code2session success
JSONObject data = dto.getJSONObject("data");
return JSON.toJavaObject(data, WxAccount.class);
}
}
jwt工具類,用於生成token簽名, token校驗 -> JwtUtil.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 10:26
* 你的指尖,擁有改變世界的力量
* 描述: jwt工具類: 生成token簽名, token校驗
*/
@Component
@SuppressWarnings("All")
public class JwtUtil {
/**
* 過時時間: 2小時
*/
private static final long EXPIRE_TIME = 7200;
/**
* 使用 appid 簽名
*/
@Value("${wx.appid}")
private String appsecret;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 根據微信用戶登錄信息建立 token
* 使用`uuid`隨機生成一個jwt-id
* 將用戶的`openId`、`session_key`連同`jwt-id`一塊兒,使用小程序的`appid`進行簽名加密並設置過時時間,最終生成`token`
* 將`"JWT-SESSION-"+jwt-id`和`token`以key-value的形式存入`redis`中,並設置相同的過時時間
* 注 : 這裏的token會被緩存到redis中,用做爲二次驗證
* redis裏面緩存的時間應該和jwt token的過時時間設置相同
*
* @param wxAccount 微信用戶信息
* @return 返回 jwt token
*/
public String sign(WxAccount account) {
//JWT 隨機ID,作爲redis驗證的key
String jwtId = UUID.randomUUID().toString();
//1 . 加密算法進行簽名獲得token
Algorithm algorithm = Algorithm.HMAC256(appsecret);
String token = JWT.create()
.withClaim("openId", account.getOpenId())
.withClaim("sessionKey", account.getSessionKey())
.withClaim("jwt-id",jwtId)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE_TIME * 1000))
.sign(algorithm);
//2 . Redis緩存JWT, 注 : 請和JWT過時時間一致
redisTemplate.opsForValue().set("JWT-SESSION-"+jwtId, token, EXPIRE_TIME, TimeUnit.SECONDS);
return token;
}
/**
* token 檢驗
* @param token
* @return bool
*/
public boolean verify(String token) {
try {
//1 . 根據token解密,解密出jwt-id , 先從redis中查找出redisToken,匹配是否相同
String redisToken = redisTemplate.opsForValue().get("JWT-SESSION-" + getClaimsByToken(token).get("jwt-id").asString());
if (!token.equals(redisToken)) {
return Boolean.FALSE;
}
//2 . 獲得算法相同的JWTVerifier
Algorithm algorithm = Algorithm.HMAC256(appsecret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("openId", getClaimsByToken(redisToken).get("openId").asString())
.withClaim("sessionKey", getClaimsByToken(redisToken).get("sessionKey").asString())
.withClaim("jwt-id",getClaimsByToken(redisToken).get("jwt-id").asString())
.build();
//3 . 驗證token
verifier.verify(token);
//4 . Redis緩存JWT續期
redisTemplate.opsForValue().set("JWT-SESSION-" + getClaimsByToken(token).get("jwt-id").asString(),
redisToken,
EXPIRE_TIME,
TimeUnit.SECONDS);
return Boolean.TRUE;
} catch (Exception e) {
//捕捉到任何異常都視爲校驗失敗
return Boolean.FALSE;
}
}
/**
* 從token解密信息
* @param token token
* @return
* @throws JWTDecodeException
*/
public Map<String, Claim> getClaimsByToken(String token) throws JWTDecodeException {
return JWT.decode(token).getClaims();
}
}
建立JwtToken,用於shiro鑑權,須要實現AuthenticationToken
-> JwtToken.java
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 10:48
* 你的指尖,擁有改變世界的力量
* 描述: 鑑權用的token,須要實現 AuthenticationToken
*/
@Data
@AllArgsConstructor
public class JwtToken implements AuthenticationToken {
private String token;
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
自定義Shiro的Realm配置,須要在Realm中實現咱們自定義的登錄及受權邏輯 -> ShiroRealm.java
import com.github.gongsir0630.shirodemo.controller.res.CodeMsg;
import com.github.gongsir0630.shirodemo.exception.ApiAuthException;
import com.github.gongsir0630.shirodemo.wx.util.JwtUtil;
import com.github.gongsir0630.shirodemo.wx.vo.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 15:28
* 你的指尖,擁有改變世界的力量
* 描述: Realm 的一個配置管理類 allRealm()方法獲得全部的realm
*/
@Component
@Slf4j
public class ShiroRealm {
@Resource
private JwtUtil jwtUtil;
/**
* 封裝全部自定義的realm規則鏈 -> shiro配置中會將規則注入到shiro的securityManager
* @return 全部自定義的realm規則
*/
public List<Realm> allRealm() {
List<Realm> realmList = new LinkedList<>();
realmList.add(authorizingRealm());
return Collections.unmodifiableList(realmList);
}
/**
* 自定義 JWT的 Realm
* 重寫 Realm 的 supports() 方法是經過 JWT 進行登陸判斷的關鍵
*/
private AuthorizingRealm authorizingRealm() {
AuthorizingRealm realm = new AuthorizingRealm() {
/**
* 當須要檢測 用戶權限 時調用此方法,例如checkRole,checkPermission之類的
* 根據業務需求自行編寫驗證邏輯
* @param principalCollection == token
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String token = principalCollection.toString();
log.info("--->>>PrincipalCollection: [{}]",token);
// todo: 自定義權限驗證, 好比role和permission驗證
return new SimpleAuthorizationInfo();
}
/**
* 默認使用此方法進行用戶名正確與否校驗: 驗證token邏輯
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String jwtToken = (String) authenticationToken.getCredentials();
String openId = jwtUtil.getClaimsByToken(jwtToken).get("openId").asString();
String sessionKey = jwtUtil.getClaimsByToken(jwtToken).get("sessionKey").asString();
if (null == openId || "".equals(openId)) {
throw new ApiAuthException(CodeMsg.NO_USER);
}
if (null == sessionKey || "".equals(sessionKey)) {
throw new ApiAuthException(CodeMsg.SESSION_KEY_ERROR);
}
if (!jwtUtil.verify(jwtToken)) {
throw new ApiAuthException(CodeMsg.TOKEN_ERROR);
}
// 將 openId 和 sessionKey 裝配到subject中
// 在 Controller 中使用 SecurityUtils.getSubject().getPrincipal() 便可獲取用戶openId
return new SimpleAuthenticationInfo(openId,sessionKey,this.getClass().getName());
}
/**
* 注意坑點 : 必須重寫此方法,否則Shiro會報錯
* 由於建立了 JWTToken 用於替換Shiro原生 token,因此必須在此方法中顯式的進行替換,不然在進行判斷時會一直失敗
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
};
realm.setCredentialsMatcher(credentialsMatcher());
return realm;
}
/**
* 注意 : 密碼校驗, 這裏由於是JWT形式,就無需密碼校驗和加密,直接讓其返回爲true(若是不設置的話,該值默認爲false,即始終驗證不經過)
*/
private CredentialsMatcher credentialsMatcher() {
// 實現boolean doCredentialsMatch(AuthenticationToken var1, AuthenticationInfo var2);
return (authenticationToken, authenticationInfo) -> true;
}
}
全部的請求都會先通過Filter
,因此咱們繼承官方的BasicHttpAuthenticationFilter
,而且重寫方法便可 -> JwtFilter.java
import com.github.gongsir0630.shirodemo.wx.vo.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 10:58
* 你的指尖,擁有改變世界的力量
* 描述: JWT核心過濾器配置
* 全部的請求都會先通過Filter,繼承官方的BasicHttpAuthenticationFilter,而且重寫鑑權的方法
* 執行流程 preHandle->isAccessAllowed->isLoginAttempt->executeLogin
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 跨域支持
* @param request 請求
* @param response 相應
* @return bool
* @throws Exception 異常
*/
@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);
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
// 判斷request是否包含 Authorization 字段
String auth = getAuthzHeader(request);
return auth != null && !"".equals(auth);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request,response)) {
// executeLogin 進入登陸邏輯
// 從request請求頭獲取 Authorization 字段
String token = getAuthzHeader(request);
log.info("--->>>JwtFilter::isAccessAllowed攔截到認證token信息:[{}]",token);
// 這裏會提交給剛剛咱們自定義的realm處理
getSubject(request,response).login(new JwtToken(token));
}
// 這裏返回true表示全部驗證結果都能經過, 在controller中可使用shiro註解限制是否須要登陸權限
// 設置true即容許遊客訪問
// 設置false則必須攜帶token進行驗證
return true;
}
}
核心配置 -> ShiroConfig.java
import com.github.gongsir0630.shirodemo.filter.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/20 16:48
* 你的指尖,擁有改變世界的力量
* 描述: shiro核心配置
*/
@Configuration
public class ShiroConfig {
/**
* SecurityManager,安全管理器,全部與安全相關的操做都會與之進行交互;
* 它管理着全部Subject,全部Subject都綁定到SecurityManager,與Subject的全部交互都會委託給SecurityManager
* DefaultWebSecurityManager :
* 會建立默認的DefaultSubjectDAO(它又會默認建立DefaultSessionStorageEvaluator)
* 會默認建立DefaultWebSubjectFactory
* 會默認建立ModularRealmAuthenticator
*/
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置realms
securityManager.setRealms(shiroRealm.allRealm());
// close session
DefaultSubjectDAO defaultSubjectDAO = (DefaultSubjectDAO) securityManager.getSubjectDAO();
DefaultSessionStorageEvaluator evaluator = (DefaultSessionStorageEvaluator) defaultSubjectDAO.getSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(Boolean.FALSE);
defaultSubjectDAO.setSessionStorageEvaluator(evaluator);
return securityManager;
}
/**
* 配置Shiro的訪問策略
*/
@Bean
public ShiroFilterFactoryBean filterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new HashMap<>(8);
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
Map<String, String> filterRuleMap = new HashMap<>(8);
//登錄相關api不須要被過濾器攔截
filterRuleMap.put("/user/login/**", "anon");
// 全部請求經過JWT Filter
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 添加註解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 添加註解依賴
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 開啓註解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
實現UserService中的login方法 -> UserServiceImpl.java
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.gongsir0630.shirodemo.mapper.UserMapper;
import com.github.gongsir0630.shirodemo.model.User;
import com.github.gongsir0630.shirodemo.service.UserService;
import com.github.gongsir0630.shirodemo.wx.model.WxAccount;
import com.github.gongsir0630.shirodemo.wx.service.WxAccountService;
import com.github.gongsir0630.shirodemo.wx.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 11:19
* 你的指尖,擁有改變世界的力量
* 描述:
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private JwtUtil jwtUtil;
@Resource
private WxAccountService wxAccountService;
@Override
public Map<String, String> login(String jsCode) {
Map<String, String> res = new HashMap<>();
WxAccount wxAccount = wxAccountService.login(jsCode);
log.info("--->>>wxAccount信息:[{}]",wxAccount);
User user = userMapper.selectById(wxAccount.getOpenId());
if (user == null) {
// todo: 用戶不存在, 提醒用戶提交註冊信息
res.put("canLogin",Boolean.FALSE.toString());
} else {
res.put("canLogin",Boolean.TRUE.toString());
}
res.put("token", jwtUtil.sign(wxAccount));
return res;
}
}
建立controller,編寫測試api -> UserController.java
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.gongsir0630.shirodemo.controller.res.CodeMsg;
import com.github.gongsir0630.shirodemo.controller.res.Result;
import com.github.gongsir0630.shirodemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
/**
* @author 碼之淚殤 GitHub: https://github.com/gongsir0630
* @date 2021/3/23 11:12
* 你的指尖,擁有改變世界的力量
* 描述: 用戶信息接口類,包含小程序登陸註冊
*/
@RestController
@Slf4j
@RequestMapping("user")
public class UserController {
@Resource
private UserService userService;
/**
* 從認證信息中獲取用戶Id: userId == openId
* @return userId
*/
private String getUserId() {
return SecurityUtils.getSubject().getPrincipal().toString();
}
/**
* 小程序用戶登陸接口: 經過js_code換取openId, 判斷用戶是否已經註冊
* @param code wx.login() 獲得的code憑證
* @return token
*/
@PostMapping("/login")
public ResponseEntity<Result<JSONObject>> login(String code) {
if (StringUtils.isBlank(code)) {
return new ResponseEntity<>(Result.fail(new CodeMsg(401,"code is empty"), null), HttpStatus.OK);
}
log.info("--->接收到來自小程序端的code:[{}]",code);
// todo: 使用 code -> wxAccountService.login() -> openId,session_key
Map<String, String> loginMap = userService.login(code);
boolean canLogin = Boolean.parseBoolean(loginMap.get("canLogin"));
String token = loginMap.get("token");
JSONObject data = new JSONObject();
data.put("token",token);
data.put("canLogin",canLogin);
log.info("--->>>返回認證信息:[{}]", data.toString());
if (!canLogin) {
// todo: 用戶不存在,提示用戶註冊
return new ResponseEntity<>(Result.fail(CodeMsg.NO_USER,data),HttpStatus.OK);
}
return new ResponseEntity<>(Result.success(data),HttpStatus.OK);
}
/**
* 使用 RequiresAuthentication 註解, 須要驗證才能訪問
* @return userId
*/
@GetMapping("/hello")
@RequiresAuthentication
public ResponseEntity<Result<JSONObject>> requireAuth() {
JSONObject data = new JSONObject();
data.put("hello",getUserId());
return new ResponseEntity<>(Result.success(data),HttpStatus.OK);
}
}
編寫小程序測試代碼獲取code
:
wx.login({
timeout: 3000,
success: (res) => {
console.log(res);
}
})
image.png
啓動 wx-java-miniapp
項目:
image.png
啓動shiro-jwt-demo
項目:
image.png
Postman測試認證:
image.png
攜帶token訪問:
image.png
以上就是基於Shiro、JWT實現微信小程序登陸完整例子的邏輯過程說明及其實現。