Based on Shiro | JWT integration WxJava to achieve WeChat applet login

Based on Shiro | JWT integration WxJava to achieve WeChat applet login

Preface

I m currently working on a graduation project, involving the development of WeChat Mini Programs. The front-end Mini Program users are required to log in as WeChat. After the login is successful, the backend returns a custom login status token to the Mini Program. Subsequent Mini Programs sending API requests need to carry the token. Access background data.

This article is a complete example of docking WeChat Mini Programs to realize custom login status, realize custom login of Mini Program, and return custom login status token to Mini Program as login credentials. The user's information is stored in the database, and the login token is cached in redis. Involved technology stack:

  • SpringBoot -> Backend Basic Environment
  • Shiro -> Security Framework
  • JWT -> encrypted token
  • MySQL -> Main database, store business data
  • MyBatis-Plus -> Operation database
  • Redis -> Cache token and other hot data
  • Lombok -> simplified development
  • FastJson -> json message processing
  • RestTemplate -> handle web requests gracefully

Project GitHub address: github.com/companyr0630...

characteristic

  • Connect WeChat applet based on WxJava to realize user login and message processing
  • Support Shiro annotation programming to maintain a high degree of flexibility
  • Use JWT for verification, fully realize stateless authentication
  • Use Redis to store custom login tokens and support expiration time
  • Support cross-domain requests

Ready to work

Basic knowledge preparation:

  • Have basic knowledge of SpringBoot and use basic annotations;
  • Understand the basic concepts of JWT (Json Web Token), and can easily operate JWT's JAVA SDK ;
  • Understand the basic concepts of Shiro: Subject, Realm, SecurityManager, etc. (It is recommended to go to the official website to learn)

other instructions:

This article only introduces the integration of shiro and jwt, the specific implementation of WeChat login is to use

RestTemplate
Call my own
wx-java-miniapp
Project, the project is based on
WxJava
Realize, support multiple small program login and message processing.

This article can use the following call processing:

//1. todo: WeChat login: code + appid -> openId + session_key //appid: Read MultiValueMap<String, Object> from the configuration file request = new LinkedMultiValueMap<>(); //Parameter package, WeChat login requires the following Parameters request.add( "code" , code); //eg: http://localhost:8081/wx/user/{appid}/login String path = url+ "/user/" +appid+ "/login" ; //Request JSONObject dto = restTemplate.postForObject(path, request, JSONObject.class); log.info( "--->>>Return from [{}] = [{}]" ,path,dto); //2. todo: Use openId and session_key generate custom login status -> token copy the code

project address:

the whole idea

First understand the official login process of the Mini Program, and the official instructions are here.

  1. Applet call
    wx.login()
    get
    code
    , Send the code to the background, the background passes
    wx-java-miniapp
    Get the user's
    openId
    with
    session_key
  2. Generate custom user status information through the jwt tool in the background
    token
    , And query in the database in the background
    openId
    Determine whether it exists, encapsulate different messages according to the query results, and finally
    token
    Return to the applet together;
  3. After that, the user must access every API request that requires permission in
    header
    Add in
    Authorization
    Field, the background will be
    token
    Check, if there is a misunderstanding, return directly
    401
    .

Token encryption description

  • use
    uuid
    Randomly generate a jwt-id
  • The user's
    openId
    ,
    session_key
    Together with
    jwt-id
    Together, using the applet
    appid
    Perform signature encryption and set the expiration time, and finally generate
    token
  • will
    "JWT-SESSION-"+jwt-id
    with
    token
    Deposit in the form of key-value
    redis
    And set the same expiration time

token verification instructions

  • Parse the jwt-id in the token
  • To
    "JWT-SESSION-"+jwt-id
    Get redisToken from redis for key
  • Parsing
    redisToken
    Carry information, regenerate the validator in the same way, and
    token
    Check and compare

Project realization

  • Project database usage
    MySQL
    As the main library, if it is
    clone
    For the project, please prepare the corresponding database and modify the configuration information before running.
  • The project uses redis cache, please install it locally before running
    redis
    , Just use the default configuration without modification.
  • Used in the project
    lombok
    To simplify development, please install the lombok plugin in idea or eclipse.

Create Maven project

Create a new SpringBoot project, modify the pom file, and add related dependencies:

<?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.companyr0630 </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: user authentication\interface authentication--> < dependency > < groupId > org.apache.shiro </groupId > < artifactId > shiro-spring </artifactId > < version > 1.4.0 </version > </dependency > <!-- jwt: token authentication--> < dependency > < groupId > com.auth0 </groupId > < artifactId >java-jwt </artifactId > < version > 3.4.1 </version > </dependency > <!-- redis: data cache--> < dependency > < groupId > org.springframework.boot </groupId > < artifactId > spring-boot-starter -data-redis </artifactId > </dependency > <!-- Introduce 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 database connection pool--> < dependency > < groupId > com.alibaba </groupId > < artifactId > druid-spring-boot-starter </artifactId > < version > 1.1.10 </version > </dependency > <!-- mybatis-plus: operating database--> < 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 driver --> < 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 > <!-- Tool: Simplify model development--> <dependency > <groupId > org.projectlombok </groupId > < artifactId > lombok </artifactId > < optional > true </optional > </dependency > <!-- Unit testing tool--> < 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.companyr0630.shirodemo.ShiroJwtDemoApplication </mainClass > </configuration > < executions > < execution > < id > repackage </id > < goals > < goal > repackage </goal > </goals > </execution > </executions > </plugin > </plugins > </build > </project > copy code

Note the JDK version: 1.8

Related configuration | Tool preparation

Configure your application.yml, mainly to configure your applet appid and url, as well as your database and redis.

# Set the log level logging: level: org.springframework.web: info com.github.companyr0630.shirodemo: debug # dev environment configuration file spring: # Database related configuration information: no need to install mysql locally, use yzhelp.top cloud database datasource : driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shiro-jwt-demo username: username password: password # redis configuration information: install redis redis locally : host: 127.0 .0 .1 port: 6379 database: 0 --- # Port number for service startup server: port: 8080 --- # micro-channel configuration applet AppID/url WX: # applet AppId AppID: AppID # from a small study program interface call address url: HTTP://localhost: 8081/WX copy the code

Description:
appid : the appid of the current applet
url: wx-java-miniapp project interface address

Configure fastJson

Configure in the startup class

fastJson
-> ShiroJwtDemoApplication.java

/** * @author companyr <a href="https://github.com/companyr0630">Code of Tears</a> * Description: Spring Boot project startup class, you can directly click the main method below to run the program */ @SpringBootApplication public class ShiroJwtDemoApplication { public static void main (String[] args) { SpringApplication.run(ShiroJwtDemoApplication.class, args); } /** * fastjson configuration injection: use Alibaba's fastjson to process json information * @return HttpMessageConverters */ @Bean public HttpMessageConverters fastJsonHttpMessageConverters () { //Message conversion object FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); //fastjson configuration FastJsonConfig config = new FastJsonConfig(); config.setSerializerFeatures(SerializerFeature.PrettyFormat); config.setDateFormat( "yyyy-MM-dd" ); //Configure injection message converter converter.setFastJsonConfig(config); //Let spring use a custom message converter return new HttpMessageConverters(converter); } } Copy code

Configure Redis

Configure Redis -> RedisConfig.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/20 14:15 * Your fingertips have the power to change the world * Description: Redis configuration * EnableCaching: enable caching */ @Configuration @EnableCaching public class RedisConfig { @Bean public the CacheManager CacheManager (RedisConnectionFactory Factory) { return RedisCacheManager.create (Factory); } } Copy code

Configure RestTemplate

Configure RestTemplate -> RestTemplateConfig.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/20 14:10 * Your fingertips have the power to change the world * Description: RestTemplate configuration class */ @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate (ClientHttpRequestFactory factory) { return new RestTemplate(factory); } @Bean public ClientHttpRequestFactory simpleClientHttpRequestFactory () { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); //The connection timeout is set to 10 seconds factory.setConnectTimeout( 1000 * 10 ); //The read timeout is in units of 60 seconds factory.setReadTimeout( 1000 * 60 ); return factory; } } Copy code

Return set package

CodeMsg.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/22 20:17 * Your fingertips have the power to change the world * Description: code and msg package */ 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 + '\'' + '}' ; } } Copy code

Result.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/20 18:45 * Your fingertips have the power to change the world * Description: * Package of output result * As long as get does not set, better encapsulation * @param <T> data generic */ 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; } /** * When successful * @param <T> data generic * @return Result */ public static <T> Result<T> success (T data) { return new Result<T>(data); } /** * Failed * @param <T> data generic * @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; } } Copy code

Exception encapsulation and handling

Custom exception -> ApiAuthException.java

import com.github.companyr0630.shirodemo.controller.res.CodeMsg; /** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/22 20:24 * Your fingertips have the power to change the world * Description: Custom exception, used to handle Api authentication failure exception information storage */ 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; } } Copy code

Global exception handling -> AppExceptionHandler.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/20 15:49 * Your fingertips have the power to change the world * Description: Global exception handling */ @RestControllerAdvice @Slf4j public class AppExceptionHandler { /** * Handling Shiro exceptions * @param e exception information * @return json */ @ExceptionHandler({ShiroException.class}) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ResponseEntity<Result<JSONObject>> handShiroException(ShiroException e) { log.error( "--->>> caught [ApiAuthException] exception: {}" , e.getMessage()); return new ResponseEntity<>(Result.fail(CodeMsg.SHIRO_ERROR, null ), HttpStatus.UNAUTHORIZED) ; } /** * Handling custom ApiAuthException exceptions * @param e exception information * @return json */ @ExceptionHandler({ApiAuthException.class}) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ResponseEntity<Result<JSONObject>> handApiAuthException(ApiAuthException e) { log.error( "--->>> caught [ApiAuthException] exception: {},{}" ,e.getCodeMsg().getCode(),e.getCodeMsg().getMsg() ); return new ResponseEntity< >(Result.fail(e.getCodeMsg(), null ), HttpStatus.UNAUTHORIZED); } } Copy code

Prepare the data source

  • Database: shiro-jwt-demo
  • Data sheet: user

Note: This is the business database, that is, the user information of our Mini Program is stored by ourselves. The first time we use WeChat public information registration by default, users can update this information by themselves and open it separately from WeChat information.

Create the corresponding entity class -> User.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/21 23:44 * Your fingertips have the power to change the world * Description: Business user information */ @Data @TableName("user") public class User { /** * Primary key, the database field is 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; } Copy code

Use MyBatis-plus to create a mapper interface -> UserMapper.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/22 19:44 * Your fingertips have the power to change the world * Description: User class mapper interface, inherited from BaseMapper (User's CRUD has been implemented) */ @Mapper public interface UserMapper extends BaseMapper < User > { } Copy code

MyBatis-Plus Configuration -> MybatisPlusConfig.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/19 17:15 * Your fingertips have the power to change the world * Description: MyBatis-Plus plug-in configuration */ @Configuration @MapperScan("com.github.companyr0630.shirodemo.mapper") public class MybatisPlusConfig { } Copy code

Create the User business interface, here is only the demonstration login -> UserService.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/22 19:49 * Your fingertips have the power to change the world * Description: User interface */ public interface UserService extends IService < User > { /** * log in * @param jsCode applet code * @return login information: including token */ Map<String, String> login (String jsCode) ; } Copy code

Create another WeChat login information object, which is mainly used to receive the openid and session_key of WeChat , as well as shiro authentication -> WxAccount.java

* @Author Tears yards War GitHub: HTTPS: //github.com/gongsir0630 * @date 2021/. 3/22 is . 19 : 58 * Your fingertips have the power to change the world * Description: WeChat authentication information */ @Data @AllArgsConstructor @NoArgsConstructor @ToString public class WxAccount { private String openId; private String sessionKey; } Copy code

Note: This class will not be used for business information interaction, so there is no need for Mapper to interact with db.

WeChat login interface, here to realize the information interaction with the WeChat server -> WxAccountService.java

* @Author Tears yards War GitHub: HTTPS: //github.com/gongsir0630 * @date 2021/. 3/22 is 20 is : 06 * Your fingertips have the power to change the world * Description: WeChat interface */ public interface WxAccountService { /** * WeChat applet user login, the complete process can refer to the official address below, in this example, it is developed according to this process * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html * 1. Enter the code in the WeChat applet. * 2. Use the wx-java-miniapp project to call the WeChat code2session interface to obtain openid and session_key * * @param code The code obtained by calling wx.login by the applet is used to call the WeChat code2session interface * @return JSONObject: contains openId and sessionKey */ WxAccount login (String code) ; } Copy code

Interface implementation logic:

  1. Read from configuration file
    appid
    with
    url
  2. Rely on the target request address
    path
    , For example, login is
    {url}/wx/user/{appid}/login
  3. Parameter package, package from applet
    code
  4. use
    RestTemplate
    Initiate a login request;
  5. Process the return set.

Code implementation -> WxAccountServiceImpl.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/20 16:12 * Your fingertips have the power to change the world * Description: WeChat interface implementation: use restTemplate to call the interface of the [wxApp] application */ @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: WeChat login: code + appid -> openId + session_key //appid: read MultiValueMap<String, Object> request = new LinkedMultiValueMap<>(); //Parameter package, WeChat login requires the following parameters request.add( "code" , code); //eg: http://localhost:8081/wx/user/{appid}/login String path = url+ "/user/" +appid+ "/login" ; //request JSONObject dto = restTemplate.postForObject(path, request, JSONObject.class); log.info( "--->>>Return from [{}] = [{}]" ,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); } } Copy code

Build JWT

jwt tool class, used to generate token signature, token verification -> JwtUtil.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/23 10:26 * Your fingertips have the power to change the world * Description: jwt tool class: generate token signature, token verification */ @Component @SuppressWarnings("All") public class JwtUtil { /** * Expiration time: 2 hours */ private static final long EXPIRE_TIME = 7200 ; /** * Sign with appid */ @Value("${wx.appid}") private String appsecret; @Autowired private StringRedisTemplate redisTemplate; /** * Create token based on WeChat user login information * Use `uuid` to randomly generate a jwt-id * Use the user's `openId`, `session_key` together with `jwt-id`, use the applet `appid` to sign and encrypt and set the expiration time, and finally generate `token` * Save `"JWT-SESSION-"+jwt-id` and `token` in the form of key-value in `redis`, and set the same expiration time * Note: The token here will be cached in redis and used as a secondary verification * The cache time in redis should be the same as the expiration time of the jwt token. * * @param wxAccount WeChat user information * @return returns jwt token */ public String sign (WxAccount account) { //JWT random ID, as the key for redis verification String jwtId = UUID.randomUUID().toString(); //1. Encryption algorithm to sign to get 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 caches JWT, note: please be consistent with JWT expiration time redisTemplate.opsForValue().set( "JWT-SESSION-" +jwtId, token, EXPIRE_TIME, TimeUnit.SECONDS); return token; } /** * token inspection * @param token * @return bool */ public boolean verify (String token) { try { //1. According to the decryption of the token, decrypt the jwt-id, first find out the redisToken from redis, whether the match is the same String redisToken = redisTemplate.opsForValue().get( "JWT -SESSION-" + getClaimsByToken(token).get( "jwt-id" ).asString()); if (!token.equals(redisToken)) { return Boolean.FALSE; } //2. Get the JWTVerifier with the same algorithm 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. Validate token verifier.verify(token); //4. Redis cache JWT renewal redisTemplate.opsForValue().set( "JWT-SESSION-" + getClaimsByToken(token).get( "jwt-id" ).asString(), redisToken, EXPIRE_TIME, TimeUnit.SECONDS); return Boolean.TRUE; } catch (Exception e) { //Catch any exception as verification failure return Boolean.FALSE; } } /** * Decrypt information from token * @param token token * @Return * @throws JWTDecodeException */ public Map<String, Claim> getClaimsByToken (String token) throws JWTDecodeException { return JWT.decode(token).getClaims(); } } Copy code

Realm configuration

Create JwtToken, used for shiro authentication, need to be implemented

AuthenticationToken
-> JwtToken.java

/** * @author GitHub: https://github.com/companyr0630 * @date 2021/3/23 10:48 * Your fingertips have the power to change the world * Description: The token used for authentication needs to implement AuthenticationToken */ @Data @AllArgsConstructor public class JwtToken implements AuthenticationToken { private String token; @Override public Object getPrincipal () { return token; } @Override public Object getCredentials () { return token; } } Copy code

To customize Shiro's Realm configuration, we need to implement our custom login and authorization logic in Realm -> ShiroRealm.java

import com.github.companyr0630.shirodemo.controller.res.CodeMsg; import com.github.companyr0630.shirodemo.exception.ApiAuthException; import com.github.companyr0630.shirodemo.wx.util.JwtUtil; import com.github.companyr0630. 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; importorg.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/companyr0630 * @date 2021/3/23 15:28 * Your fingertips have the power to change the world * Description: A configuration management class allRealm() method of Realm gets all realm */ @Component @Slf4j public class ShiroRealm { @Resource private JwtUtil jwtUtil; /** * Encapsulate all custom realm rule chains -> shiro configuration will inject the rules into shiro's securityManager * @return all custom realm rules */ public List<Realm> allRealm () { List<Realm> realmList = new LinkedList<>(); realmList.add(authorizingRealm()); return Collections.unmodifiableList(realmList); } /** * Custom JWT Realm * Rewriting Realm's supports() method is the key to login judgment through JWT */ private AuthorizingRealm authorizingRealm () { AuthorizingRealm realm = new AuthorizingRealm() { /** * Call this method when you need to detect user permissions, such as checkRole, checkPermission and the like * Write your own verification logic according to business needs * @param principalCollection == token */ @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) { String token = principalCollection.toString(); log.info( "--->>>PrincipalCollection: [{}]" ,token); //todo: custom authorization verification, such as role and permission verification return new SimpleAuthorizationInfo(); } /** * By default, this method is used to verify the correctness of the user name: verify the token logic */ @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); } //Assemble the openId and sessionKey into the subject //Use SecurityUtils.getSubject().getPrincipal() in the Controller to get the user openId return new SimpleAuthenticationInfo(openId,sessionKey, this .getClass().getName()); } /** * Pay attention to the pit: This method must be rewritten, otherwise Shiro will report an error * Because the JWTToken is created to replace the Shiro native token, it must be replaced explicitly in this method, otherwise the judgment will always fail */ @Override public boolean supports (AuthenticationToken token) { return token instanceof JwtToken; } }; realm.setCredentialsMatcher(credentialsMatcher()); return realm; } /** * Note: Password verification, here because it is in JWT form, there is no need for password verification and encryption, just let it return to true (if not set, the value defaults to false, that is, the verification always fails) */ private CredentialsMatcher credentialsMatcher () { //Implement boolean doCredentialsMatch(AuthenticationToken var1, AuthenticationInfo var2); return (authenticationToken, authenticationInfo) -> true ; } } Copy code

Rewrite filter

All requests will go through first

Filter
, So we inherit the official
BasicHttpAuthenticationFilter
, And rewrite the method -> JwtFilter.java

import com.github.companyr0630.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/companyr0630 * @date 2021/3/23 10:58 * Your fingertips have the power to change the world * Description: JWT core filter configuration * All requests will go through the Filter first, inherit the official BasicHttpAuthenticationFilter, and override the authentication method * Execution process preHandle->isAccessAllowed->isLoginAttempt->executeLogin */ @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { /** * Cross-domain support * @param request request * @param response corresponding * @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" )); //When cross-domain, an option request will be sent first, here we will directly return to normal for the option request Status 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) { //Determine whether the request contains the Authorization field String auth = getAuthzHeader(request); return auth != null &&! "" .equals(auth); } @Override protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request,response)) { //executeLogin enters the login logic //Get the Authorization field from the request header String token = getAuthzHeader(request); log.info( "--->>>JwtFilter::isAccessAllowed intercepted authentication token information: [{}]" ,token); //here will be submitted to our customized realm for processing getSubject(request,response). login( new JwtToken(token)); } //Return true here to indicate that all verification results can be passed. In the controller, you can use shiro annotations to restrict whether login permissions are required. //Set true to allow visitors to access //Set false, you must carry the token for verification return true ; } } Copy code

Shiro core configuration

Core Configuration -> ShiroConfig.java

  • Configure realm rule chain
  • Configure access policy: url and filter
  • Enable shiro annotation support
import com.github.companyr0630.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; importorg.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/companyr0630 * @date 2021/3/20 16:48 * Your fingertips have the power to change the world * Description: shiro core configuration */ @Configuration public class ShiroConfig { /** * SecurityManager, security manager, all security-related operations will interact with it; * It manages all Subjects, all Subjects are bound to SecurityManager, and all interactions with Subjects will be delegated to SecurityManager * DefaultWebSecurityManager: * Will create the default DefaultSubjectDAO (it will create DefaultSessionStorageEvaluator by default) * DefaultWebSubjectFactory will be created by default * ModularRealmAuthenticator will be created by default */ @Bean public DefaultWebSecurityManager securityManager (ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //set 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; } /** * Configure Shiro's access policy */ @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 ); //Login related APIs do not need to be intercepted by filters filterRuleMap.put( "/user/login/**" , "anon" ); //All requests Pass JWT Filter filterRuleMap.put( "/**" , "jwt" ); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } /** * Add annotation support */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator () { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass( true ); return advisorAutoProxyCreator; } /** * Add annotation dependency */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor () { return new LifecycleBeanPostProcessor(); } /** * Open comments */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor (DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } } Copy code

verification

Implement the login method in UserService -> UserServiceImpl.java

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.github.companyr0630.shirodemo.mapper.UserMapper; import com.github.companyr0630.shirodemo.model.User; import com.github.companyr0630.shirodemo. service.UserService; import com.github.companyr0630.shirodemo.wx.model.WxAccount; import com.github.companyr0630.shirodemo.wx.service.WxAccountService; import com.github.companyr0630.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/companyr0630 * @date 2021/3/23 11:19 * Your fingertips have the power to change the world * Description: */ @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 information:[{}]" ,wxAccount); User user = userMapper.selectById(wxAccount.getOpenId()); if (user == null ) { //todo: user does not exist, remind user to submit registration information res.put( "canLogin" ,Boolean.FALSE.toString()); } else { res.put( "canLogin" ,Boolean.TRUE.toString()); } res.put( "token" , jwtUtil.sign(wxAccount)); return res; } } Copy code

Create controller, write test api -> UserController.java

import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.github.companyr0630.shirodemo.controller.res.CodeMsg; import com.github.companyr0630.shirodemo.controller.res. Result; import com.github.companyr0630.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; importorg.springframework.web.bind.annotation.GetMapping; importorg.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/companyr0630 * @date 2021/3/23 11:12 * Your fingertips have the power to change the world * Description: User information interface class, including small program login and registration */ @RestController @Slf4j @RequestMapping("user") public class UserController { @Resource private UserService userService; /** * Obtain the user ID from the authentication information: userId == openId * @return userId */ private String getUserId () { return SecurityUtils.getSubject().getPrincipal().toString(); } /** * Mini program user login interface: exchange openId through js_code to determine whether the user is already registered * @param code wx.login() code certificate * @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( "---> received code from the applet: [{}]" ,code); //todo: use 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( "--->>>Return authentication information: [{}]" , data.toString()); if (!canLogin) { //todo: user does not exist, prompt user to register return new ResponseEntity<> (Result.fail(CodeMsg.NO_USER,data),HttpStatus.OK); } return new ResponseEntity<>(Result.success(data),HttpStatus.OK); } /** * Use RequiresAuthentication annotation, need authentication to access * @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); } } Copy code

Write small program test code to obtain

code
:

wx.login({ timeout : 3000 , success : ( res ) => { console .log(res); } }) Copy code

start up

project:

start up

shiro-jwt-demo
project:

Postman test certification:

Access with token:

At last

The above is the logical process description and implementation of a complete example of WeChat applet login based on Shiro and JWT.