常见的单token登录方案

现在主流的单token方案为jwttoken和redis token

常用的跟jwt token集成框架有shrio、spring security、aop切面。redis也能跟这三者集成。跟redis相比,jwt token比较难注销,得等到有效期过了才行,实际根据项目需求来就行。

简单介绍如下,理论就不讲太多,看代码吧。

一、jwt+shrio实现

1.依赖导入

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.5.3</version></dependency>

2.代码编写

工具类JwtOperator.java

package com.vvvtimes.demo.util;import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
public class JwtOperator {/*** 秘钥* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt*/@Value("${secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")private String secret;/*** 有效期,单位秒* - 默认2周*/@Value("${expire-time-in-second:1209600}")private Long expirationTimeInSecond;/*** 从token中获取claim** @param token token* @return claim*/public Claims getClaimsFromToken(String token) {try {return Jwts.parser().setSigningKey(this.secret.getBytes()).parseClaimsJws(token).getBody();} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {log.error("token解析错误", e);throw new IllegalArgumentException("Token invalided.");}}/*** 获取token的过期时间** @param token token* @return 过期时间*/public Date getExpirationDateFromToken(String token) {return getClaimsFromToken(token).getExpiration();}/*** 判断token是否过期** @param token token* @return 已过期返回true,未过期返回false*/private Boolean isTokenExpired(String token) {Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}/*** 计算token的过期时间** @return 过期时间*/private Date getExpirationTime() {return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);}/*** 为指定用户生成token** @param claims 用户信息* @return token*/public String generateToken(Map<String, Object> claims) {Date createdTime = new Date();Date expirationTime = this.getExpirationTime();byte[] keyBytes = secret.getBytes();SecretKey key = Keys.hmacShaKeyFor(keyBytes);return Jwts.builder().setClaims(claims).setIssuedAt(createdTime).setExpiration(expirationTime)// 你也可以改用你喜欢的算法// 支持的算法详见:https://github.com/jwtk/jjwt#features.signWith(key, SignatureAlgorithm.HS256).compact();}/*** 判断token是否非法** @param token token* @return 未过期返回true,否则返回false*/public Boolean validateToken(String token) {return !isTokenExpired(token);}/*public static void main(String[] args) {// 1. 初始化JwtOperator jwtOperator = new JwtOperator();jwtOperator.expirationTimeInSecond = 1209600L;jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt";// 2.设置用户信息HashMap<String, Object> objectObjectHashMap = Maps.newHashMap();objectObjectHashMap.put("id", "1");// 测试1: 生成tokenString token = jwtOperator.generateToken(objectObjectHashMap);// 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQSystem.out.println(token);// 将我改成上面生成的token!!!String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE2OTU4OTc2NTQsImV4cCI6MTY5NzEwNzI1NH0.XK730y8tMwAjjTdLqYV4FArwX-_7l0oUvtDmJX2wmLE";// 测试2: 如果能token合法且未过期,返回trueBoolean validateToken = jwtOperator.validateToken(someToken);System.out.println(validateToken);// 测试3: 获取用户信息Claims claims = jwtOperator.getClaimsFromToken(someToken);System.out.println(claims);// 将我改成你生成的token的第一段(以.为边界)String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";// 测试4: 解密Headerbyte[] header = Base64.decodeBase64(encodedHeader.getBytes());System.out.println(new String(header));// 将我改成你生成的token的第二段(以.为边界)String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE2OTU4OTc2NTQsImV4cCI6MTY5NzEwNzI1NH0";// 测试5: 解密Payloadbyte[] payload = Base64.decodeBase64(encodedPayload.getBytes());System.out.println(new String(payload));// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");}*/
}

配置类ShiroBeanConfig

package com.vvvtimes.demo.auth.config;import com.vvvtimes.demo.auth.ShiroAuthFilter;
import com.vvvtimes.demo.auth.ShiroAuthRealm;
import org.apache.shiro.mgt.SecurityManager;
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 javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;/*** shrio配置类*/
@Configuration
public class ShiroBeanConfig {@Bean("securityManager")public SecurityManager securityManager(ShiroAuthRealm shiroAuthRealm){//ShiroAuthRealm是自定义reamlDefaultWebSecurityManager manager=new DefaultWebSecurityManager();manager.setRealm(shiroAuthRealm);return manager;}@Bean("shiroFilter")public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);//装载自定义的token过滤器Map<String, Filter> tokenFilterMap=new HashMap<>();tokenFilterMap.put("oauth2",new ShiroAuthFilter());shiroFilter.setFilters(tokenFilterMap);//普通路径过滤规则Map<String, String> filterMap = new LinkedHashMap<>();//测试相关filterMap.put("/test", "anon");//登录相关filterMap.put("/login/doLogin", "anon");filterMap.put("/login/logout", "anon");filterMap.put("/login/noLogin", "anon");filterMap.put("/login/captcha", "anon");//临时测试//filterMap.put("/user/addUser", "anon");//自定义的token过滤器拦截其他任何请求filterMap.put("/**", "oauth2");shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}@Bean("lifecycleBeanPostProcessor")public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();proxyCreator.setProxyTargetClass(true);return proxyCreator;}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}}

shrio过滤器类

package com.vvvtimes.demo.auth;import cn.hutool.json.JSONUtil;
import com.vvvtimes.demo.common.dto.RestResponse;
import com.vvvtimes.demo.vo.UserInfo;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
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;
import java.io.IOException;/*** Shiro过滤器类*/
public class ShiroAuthFilter extends AuthenticatingFilter {@Overrideprotected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {String token = getToken((HttpServletRequest) servletRequest);if(StringUtils.hasText(token)){//ShiroAuthToken是自己定义实现AuthenticationToken接口的tokenreturn new ShiroAuthToken(token);}//executeLogin调用该方法token==null就抛异常,我们在onAccessDenied入口//重写方法没有token就提前返回并携带原因return null;}@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {return false;}/*** 登录失败,重写方法返回失败原因* @param token* @param e* @param request* @param response* @return false*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {//重写登录失败,把登录失败的原因返回给前端HttpServletResponse httpResponse = (HttpServletResponse) response;httpResponse.setContentType("application/json;charset=utf-8");httpResponse.setHeader("Access-Control-Allow-Credentials", "true");httpResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest)request).getHeader("Origin"));httpResponse.setCharacterEncoding("utf-8");httpResponse.setHeader("ContentType","application/json;charset=utf-8");httpResponse.setContentType("application/json;charset=utf-8");try {//处理登录失败的异常Throwable throwable = e.getCause() == null ? e : e.getCause();//返回401 new JsonResult.error(throwable.getMessage())RestResponse<String> restResponse = new RestResponse<>();restResponse.setStatus(401);restResponse.setMessage("token校验失败");String json = JSONUtil.toJsonStr(restResponse);httpResponse.getWriter().print(json);} catch (IOException e1) {e1.printStackTrace();}return false;}/*** 程序入口 检验token是否携带,没携带就返回提示没有token* @param servletRequest* @param servletResponse* @return* @throws Exception*/@Overrideprotected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {//获取用户tokenString token = getToken((HttpServletRequest) servletRequest);//如果token不存在提前返回401if(!StringUtils.hasText(token)){HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;httpResponse.setHeader("Access-Control-Allow-Credentials", "true");httpResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest)servletRequest).getHeader("Origin"));//new JsonResult.error(invalid token)httpResponse.setCharacterEncoding("utf-8");httpResponse.setHeader("ContentType","application/json;charset=utf-8");httpResponse.setContentType("application/json;charset=utf-8");RestResponse<String> restResponse = new RestResponse<>();restResponse.setStatus(401);restResponse.setMessage("token不存在");String json = JSONUtil.toJsonStr(restResponse);httpResponse.getWriter().print(json);return false;}//executeLogin方法会校验createToken是否返回了非null AuthenticationToken//上面我们提前校验提前返回信息给前端return executeLogin(servletRequest, servletResponse);}// 处理跨域预检请求 preflight@Overrideprotected 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"));// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());return false;}return super.preHandle(request, response);}/*** 方法获取用户的token* @param request* @return token*/public String getToken(HttpServletRequest request){//获取请求头String token = request.getHeader("token");//没有请求头就从参数里拿if (!StringUtils.hasText(token)){token=request.getParameter("token");}return token;}
}

shrio realm类

package com.vvvtimes.demo.auth;import com.vvvtimes.demo.mapper.UserMapper;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;/*** shrioRealm类*/
@Component
@Slf4j
public class ShiroAuthRealm extends AuthorizingRealm {@Resourceprivate UserMapper userMapper;@Autowiredprivate JwtOperator jwtOperator;/*** 判断是否支持token的类型 ****important***** 每一个Ream都有一个supports方法,用于检测是否支持此Token,默认的采用了return false* @param token* @return*/@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof ShiroAuthToken;}/*** 授权 集合会与 @RequiresPermissions()声明的权限方法匹配,通常不调用权限的方法不会执行* @param principalCollection* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {Map<String, Object> user = (Map<String, Object>) principalCollection.getPrimaryPrincipal();log.info("授权方法检索权限:当前的用户="+user);String userid =  (String) user.get("id");String username = (String) user.get("username");//权限集合Set<String> permsSet=new HashSet<>();//根据用户的信息查权限存入setif("123".equals(userid)){permsSet.add("sys:user");}SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setStringPermissions(permsSet);return info;}/*** 认证,登录的时候会调用* 调用时机* Subject subject = SecurityUtils.getSubject();* subject.login(token);* 在自定义token过滤器的executeLogin方法也会调用到上面两行,所以也会执行到下面的doGetAuthenticationInfo,每次请求都会认证* authenticationToken能强转成字符串,因为用的是自定义的String类型的token* @param authenticationToken* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String token = (String) authenticationToken.getPrincipal();//通过token到redis缓存里获取到对应用户userMap<String, Object> userEntity=new HashMap<>();if(jwtOperator.validateToken(token)){Claims claims = jwtOperator.getClaimsFromToken(token);userEntity.put("id",claims.get("id"));userEntity.put("username",claims.get("username"));log.info("认证token");//将 token id username 存入上下文RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = attributes.getRequest();request.setAttribute("token",token);request.setAttribute("id",claims.get("id"));request.setAttribute("username",claims.get("username"));}else{log.warn("token失效");throw new IncorrectCredentialsException("token失效");}SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userEntity, token, getName());return info;}
}

自定义token

package com.vvvtimes.demo.auth;import org.apache.shiro.authc.AuthenticationToken;/*** 自定义token,在自定义token过滤器里使用*/
public class ShiroAuthToken implements AuthenticationToken {private String token;public ShiroAuthToken(String token){this.token=token;}@Overridepublic String getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}

登录方法,获取token和用户信息

/*** 登录操作** @return*/@RequestMapping(value = "/doLogin", method = {RequestMethod.GET, RequestMethod.POST})public RestResponse<UserInfo> doLogin(@RequestBody LoginData entity) {RestResponse<UserInfo> restResponse = new RestResponse<>();LoginData loginData = entity;String username = loginData.getUsername();String password = loginData.getPassword();Boolean rememberMe = loginData.getRememberMe();User loginUser = userService.findUserByName(username);if (loginUser == null) {restResponse.setStatus(CommonConstants.USER_NOT_EXIST);return restResponse;}//盐String salt = loginUser.getSalt();String dbPassword = loginUser.getPassword();String encryptedText = DigestUtil.md5Hex(password + salt);if (dbPassword.equals(encryptedText)) {Map<String, Object> userInfoClaims = new HashMap<>();userInfoClaims.put("id", loginUser.getId().toString());userInfoClaims.put("username", loginUser.getUsername());userInfoClaims.put("role", "user");userInfoClaims.put("avatarUrl", loginUser.getAvatarUrl());String token = jwtOperator.generateToken(userInfoClaims);//String token = JWTUtil.sign(username, password, loginUser.getUuid(), rememberMe);UserInfo userInfo = new UserInfo();userInfo.setToken(token);userInfo.setUser(loginUser);restResponse.setResult(userInfo);} else {restResponse.setStatus(CommonConstants.USER_LOGIN_FILED);return restResponse;}return restResponse;}

查询方法,查询用户列表

//controller层/*** 查询人员列表*** @return* @throws NoSuchFieldException*/@RequestMapping(value = "/getUserList", method = {RequestMethod.POST, RequestMethod.GET})public @ResponseBodyRestResponse<PageInfo<UserVo>> getUserList(@RequestBody BasePageRequest<UserQueryVo> entity) {return service.getUserList(entity);}//service层public RestResponse<PageInfo<UserVo>> getUserList(BasePageRequest<UserQueryVo> entity) {RestResponse<PageInfo<UserVo>> result = new RestResponse<>();UserQueryVo queryVo = entity.getEntity();PageHelper.startPage(entity.getPageNum(), entity.getPageSize());List<UserVo> userVoList = userMapper.getUserList(queryVo);result.setResult(new PageInfo<>(userVoList));return result;}
//mapper层List<UserVo> getUserList(UserQueryVo queryVo);
//mapper xml层<select id="getUserList" resultType="com.vvvtimes.demo.vo.UserVo">select`id`, `username`, `salt`, `password`, `wx_id`, `wx_nickname`, `roles`, `avatar_url`, `create_time`, `update_time`from user a<where><if test="username != null  and username != '' ">and a.username  like concat('%', #{username}, '%')</if></where></select>

3.接口验证

验证登录接口

curl --location 'http://localhost:9080/login/doLogin' \
--header 'Content-Type: application/json' \
--data '{"username":"admin","password":"admin"
}'

验证查询列表接口

curl --location 'http://localhost:9080/user/getUserList' \
--header 'token: adc799d0341a4c84ab5c77c3504d1328' \
--header 'Content-Type: application/json' \
--data '{"username":"user1","password":"pass1"
}'

二、jwt+spring security实现

1.依赖导入

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

2.代码编写

config类

package com.vvvtimes.demo.auth.config;import com.vvvtimes.demo.auth.TokenAuthenticateFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@EnableWebSecurity
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.sessionManagement()// 设置 session 为无状态,因为基于 token 不需要 session.sessionCreationPolicy(SessionCreationPolicy.STATELESS).sessionFixation().none().and().authorizeRequests().antMatchers("/login/**").permitAll().anyRequest().authenticated().and().csrf().disable().addFilterBefore(new TokenAuthenticateFilter(), UsernamePasswordAuthenticationFilter.class);}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/login/**");}
}

token过滤器类

package com.vvvtimes.demo.auth;import cn.hutool.json.JSONUtil;
import com.vvvtimes.demo.common.dto.RestResponse;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TokenAuthenticateFilter extends OncePerRequestFilter {private JwtOperator jwtOperator;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取用户tokenString token = getToken(request);try{//jwt解析tokenif(jwtOperator.validateToken(token)){Claims claims = jwtOperator.getClaimsFromToken(token);log.info("认证token");//将 token id username 存入上下文request.setAttribute("token",token);request.setAttribute("id",claims.get("id"));request.setAttribute("username",claims.get("username"));}else{log.warn("token失效");writeFailureResponse(response,request);return;}}catch (NullPointerException exception){log.warn("token异常NullPointerException");writeFailureResponse(response,request);return;}filterChain.doFilter(request, response);}private void writeFailureResponse(HttpServletResponse response,HttpServletRequest request) throws IOException {response.setContentType("application/json;charset=utf-8");response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));response.setCharacterEncoding("utf-8");response.setHeader("ContentType","application/json;charset=utf-8");response.setContentType("application/json;charset=utf-8");RestResponse<String> restResponse = new RestResponse<>();restResponse.setStatus(401);restResponse.setMessage("token校验失败");String json = JSONUtil.toJsonStr(restResponse);response.getWriter().print(json);}/*** 方法获取用户的token* @param request* @return token*/public String getToken(HttpServletRequest request){//获取请求头String token = request.getHeader("token");//没有请求头就从参数里拿if (!StringUtils.hasText(token)){token=request.getParameter("token");}return token;}}

其他代码 工具类,登录 查列表 跟shrio实现一样,不赘述

3.接口验证

跟shrio一样

三、jwt+aop实现

1.依赖导入

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

2.代码编写

定义注解

package com.vvvtimes.demo.auth;public @interface CheckLogin {
}

实现注解

package com.vvvtimes.demo.auth;import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;@Aspect
@Component
public class CheckLoginAspect {@Autowiredprivate JwtOperator jwtOperator;@Around("@annotation(com.vvvtimes.demo.auth.CheckLogin)")public  Object checkLogin(ProceedingJoinPoint point) {try {// 1.从header里面获取tokenRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = attributes.getRequest();String token = request.getHeader("token");// 2.校验token是否合法&是否过期,如果不合法或已过期 直接抛出异常,如果合法放行Boolean isValid = jwtOperator.validateToken(token);if (!isValid ) {throw new AOPSecurityException("token不合法");}//3如果校验成功,那么将客户信息 设置到 request的attribute里面Claims claims = jwtOperator.getClaimsFromToken(token);request.setAttribute("id",claims.get("id"));request.setAttribute("username",claims.get("username"));request.setAttribute("role",claims.get("role"));request.setAttribute("avatarUrl",claims.get("avatarUrl"));return point.proceed();} catch (Throwable throwable) {throw new AOPSecurityException("token不合法");}}
}

处理异常

package com.vvvtimes.demo.auth;import cn.hutool.core.lang.copier.Copier;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.common.dto.BaseResponse;
import com.vvvtimes.demo.common.dto.RestResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHanlder {@ExceptionHandler(AOPSecurityException.class)public ResponseEntity<RestResponse<String>> error(AOPSecurityException e) {log.warn("发生AOPSecurityException异常",e);RestResponse<String> restResponse = new RestResponse<>();restResponse.setStatus(401);restResponse.setMessage("token校验失败");ResponseEntity<RestResponse<String>> response = new ResponseEntity<RestResponse<String>>(restResponse,HttpStatus.UNAUTHORIZED);return response;}
}

自定义异常类

package com.vvvtimes.demo.auth.exception;public class AOPSecurityException extends RuntimeException {public AOPSecurityException(String string) {super(string);}
}

使用时注意在需要token的方法上加上CheckLogin注解

 /*** 查询人员列表*** @return* @throws NoSuchFieldException*/@CheckLogin@RequestMapping(value = "/getUserList", method = {RequestMethod.POST, RequestMethod.GET})public @ResponseBodyRestResponse<PageInfo<UserVo>> getUserList(@RequestBody BasePageRequest<UserQueryVo> entity) {return service.getUserList(entity);}

其他工具类 登录 查列表代码与shrio方式一样

3.接口验证

跟shrio方式一样

四、redis token+aop实现

相比较来说,redis只需要参照实现前面的jwt工具类就行了,这里做了个简单的限制,限制单用户只能生成10个不同的token,如果多生成,则注销最早的token

1.加依赖

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.3</version></dependency>

2.写配置

spring:redis:host: localhostport: 6379

这里配置redis密码,生产环境强烈配置密码,见过太多redis入侵了

3.写代码

redis工具类

package com.vvvtimes.demo.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;import java.util.Map;
import java.util.concurrent.TimeUnit;@Component
public class RedisUtil {@Autowiredprivate RedisTemplate redisTemplate;@Autowired(required = false)public void setRedisTemplate(RedisTemplate redisTemplate) {RedisSerializer stringSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringSerializer);redisTemplate.setValueSerializer(stringSerializer);redisTemplate.setHashKeySerializer(stringSerializer);redisTemplate.setHashValueSerializer(stringSerializer);this.redisTemplate = redisTemplate;}/*** 获取hashKey对应的所有键值** @param key 键* @return 对应的多个键值*/public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);}/*** HashSet 并设置时间** @param key  键* @param map  对应多个键值* @param time 时间(秒)* @return true成功 false失败*/public boolean hmset(String key, Map<String, Object> map, long time) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return true;} catch (Exception e) {e.printStackTrace();return false;}}public boolean rpush(String key, String value, long time) {try {redisTemplate.opsForList().rightPush(key, value);if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return true;} catch (Exception e) {e.printStackTrace();return false;}}public Object lpop(String key) {return redisTemplate.opsForList().leftPop(key);}public Long llen(String key) {return redisTemplate.opsForList().size(key);}public Long getExpire(String token) {return redisTemplate.getExpire(token);}public Boolean delete(String token) {return redisTemplate.delete(token);}public boolean set(String key, String value, long time) {try {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);return true;} catch (Exception e) {e.printStackTrace();return false;}}public Object get(String key) {return redisTemplate.opsForValue().get(key);}/** 设置key有效期*/public boolean expire(String key,long time) {return redisTemplate.expire(key, time, TimeUnit.SECONDS);}}

redistoken工具类

package com.vvvtimes.demo.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;@Component
public class RedisTokenOperator {/*** 有效期,单位秒* - 默认2周*/@Value("${redistoken.expire-time-in-second:1209600}")private Long expirationTimeInSecond;public static final String TOKEN_PREFIX_KEY = "token:";public static final String USER_PREFIX_KEY = "user:";public static final Integer SINGLE_USER_MAX_TOKEN_SIZE = 10;/*@Autowiredprivate RedisTemplate redisTemplate;*/@Autowiredprivate RedisUtil redisUtil;/** 从token中获取UserMap*/public Map<Object, Object> getUserMapFromToken(String token) {String key = "token:" + token;return redisUtil.hmget(key);}/** 从token中获取过期日*/public Date getExpirationDateFromToken(String token) {Long expire = redisUtil.getExpire(token);return new Date(System.currentTimeMillis() + expire * 1000);}/** 判断token是否过期*/private Boolean isTokenExpired(String token) {String key = TOKEN_PREFIX_KEY + token;Map<Object, Object> hmget = redisUtil.hmget(key);if (hmget != null && hmget.size() > 0) {return false;}return true;}/** 生成token*/public String generateToken(Map<String, Object> map) {String userId = (String) map.get("id");String username = (String) map.get("username");String role = (String) map.get("role");String avatarUrl = (String) map.get("avatarUrl");//如果之前生成过,则用之前的tokenString userKey = USER_PREFIX_KEY + userId;Long oldTokenSize = redisUtil.llen(userKey);//多设备登录达到最大值if (oldTokenSize != null && oldTokenSize >= SINGLE_USER_MAX_TOKEN_SIZE) {while(oldTokenSize >= SINGLE_USER_MAX_TOKEN_SIZE){//取出最早的token,注销String oldToken = (String) redisUtil.lpop(userKey);//注销tokenkeyString oldTokenKey = TOKEN_PREFIX_KEY + oldToken;redisUtil.delete(oldTokenKey);oldTokenSize = redisUtil.llen(userKey);}}Map<String, Object> redisMap = new HashMap<>();redisMap.put("id", userId);redisMap.put("username", username);redisMap.put("role", role);redisMap.put("avatarUrl", avatarUrl);// 生成token并缓存用户信息String token = UUID.randomUUID().toString().replaceAll("-", "");String key = TOKEN_PREFIX_KEY + token;redisUtil.hmset(key, redisMap, expirationTimeInSecond);redisUtil.rpush(userKey, token, expirationTimeInSecond);return token;}/** 判断key是否有效*/public Boolean validateToken(String token) {return !isTokenExpired(token);}}

定义注解

package com.vvvtimes.demo.auth;public @interface CheckLogin {
}

实现注解

package com.vvvtimes.demo.auth;import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.util.RedisTokenOperator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.Map;@Aspect
@Component
public class CheckLoginAspect {@Autowiredprivate RedisTokenOperator redisTokenOperator;@Around("@annotation(com.vvvtimes.demo.auth.CheckLogin)")public  Object checkLogin(ProceedingJoinPoint point) {try {// 1.从header里面获取tokenRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = attributes.getRequest();String token = request.getHeader("token");// 2.校验token是否合法&是否过期,如果不合法或已过期 直接抛出异常,如果合法放行Boolean isValid = redisTokenOperator.validateToken(token);if (!isValid ) {throw new AOPSecurityException("token不合法");}//3如果校验成功,那么将客户信息 设置到 request的attribute里面Map<Object, Object> claims = redisTokenOperator.getUserMapFromToken(token);request.setAttribute("id",claims.get("id"));request.setAttribute("username",claims.get("username"));request.setAttribute("role",claims.get("role"));request.setAttribute("avatarUrl",claims.get("avatarUrl"));return point.proceed();} catch (Throwable throwable) {throw new AOPSecurityException("token不合法");}}
}

处理异常

package com.vvvtimes.demo.auth;import cn.hutool.core.lang.copier.Copier;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.common.dto.BaseResponse;
import com.vvvtimes.demo.common.dto.RestResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHanlder {@ExceptionHandler(AOPSecurityException.class)public ResponseEntity<RestResponse<String>> error(AOPSecurityException e) {log.warn("发生AOPSecurityException异常",e);RestResponse<String> restResponse = new RestResponse<>();restResponse.setStatus(401);restResponse.setMessage("token校验失败");ResponseEntity<RestResponse<String>> response = new ResponseEntity<RestResponse<String>>(restResponse,HttpStatus.UNAUTHORIZED);return response;}
}

定义异常

package com.vvvtimes.demo.auth.exception;public class AOPSecurityException extends RuntimeException {public AOPSecurityException(String string) {super(string);}
}

登录方法没有Claim类了,一并更改

/*** 登录操作** @return*/@RequestMapping(value = "/doLogin", method = {RequestMethod.GET, RequestMethod.POST})public RestResponse<UserInfo> doLogin(@RequestBody LoginData entity) {RestResponse<UserInfo> restResponse = new RestResponse<>();LoginData loginData = entity;String username = loginData.getUsername();String password = loginData.getPassword();Boolean rememberMe = loginData.getRememberMe();User loginUser = userService.findUserByName(username);if (loginUser == null) {restResponse.setStatus(CommonConstants.USER_NOT_EXIST);return restResponse;}//盐String salt = loginUser.getSalt();String dbPassword = loginUser.getPassword();String encryptedText = DigestUtil.md5Hex(password + salt);if (dbPassword.equals(encryptedText)) {Map<String, Object> userInfoClaims = new HashMap<>();userInfoClaims.put("id", loginUser.getId().toString());userInfoClaims.put("username", loginUser.getUsername());userInfoClaims.put("role", "user");userInfoClaims.put("avatarUrl", loginUser.getAvatarUrl());String token = redisTokenOperator.generateToken(userInfoClaims);//String token = JWTUtil.sign(username, password, loginUser.getUuid(), rememberMe);UserInfo userInfo = new UserInfo();userInfo.setToken(token);userInfo.setUser(loginUser);restResponse.setResult(userInfo);} else {restResponse.setStatus(CommonConstants.USER_LOGIN_FILED);return restResponse;}return restResponse;}

其他代码跟shrio没有区别

4.接口验证

跟shrio一致。

redis里看到的token如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/179583.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二叉树OJ题汇总

本专栏内容为&#xff1a;leetcode刷题专栏&#xff0c;记录了leetcode热门题目以及重难点题目的详细记录 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;Leetcode &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &…

Nacos报错Connection refused (Connection refused)(最后原因醉了,非常醉)

目录 一、问题产生二、排查思路1.nacos拒绝连接&#xff0c;排查思路&#xff1a;2.Nacos启动成功但是拒绝连接的几种原因&#xff1a; 三、实操过程&#xff08;着急解决问题直接看这个&#xff09;1.启动Nacos2.查看Nacos启动日志3.根据日志处理问题4.修改Nacos5.重启Nacos 一…

HT5010 音频转换器工作原理

HT5010是一款低成B的立体声DA转换器&#xff0c;内部集成了内插滤波器、DA转换器和输出模拟滤波等电路。其可支持多种音频数字输入格式&#xff0c;支持24-bit字节。 该HT5010 基于一个多比特位的Δ-Σ调制器&#xff0c;将数字信号转化成两个声道的模拟信号并经过模拟滤波器滤…

天体学爱好者基础知识-太阳系//未完待续,业余者的学习

难过的时候&#xff0c;仰望天空吧&#xff0c;人类有时候&#xff0c;做的事情真的太愚昧且无聊了&#xff0c;渺小的尘埃&#xff0c;也可以飘际宇宙。 太阳系-八大行星 卫星围绕着恒星公转。行星必须围绕着恒星公转。 什么是行星&#xff1f;行星和恒星、卫星有什么区别&am…

Azure 机器学习 - 使用 Visual Studio Code训练图像分类 TensorFlow 模型

了解如何使用 TensorFlow 和 Azure 机器学习 Visual Studio Code 扩展训练图像分类模型来识别手写数字。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0c;复旦机器人智能实验室成员…

静态库的概念及影响

1、目标文件的生成&#xff1a; 由编译器针对源文件编译生成&#xff0c;生成的.o或者.so(动态库)或者.a(静态库)也可以看作是目标文件&#xff1b; 2、静态库的生成&#xff1a; 由给定的一堆目标文件以及链接选项&#xff0c;链接器可以生成两种库&#xff0c;分别是静态库…

学 Java 怎么进外企?

作者&#xff1a;**苍何&#xff0c;CSDN 2023 年 实力新星&#xff0c;前大厂高级 Java 工程师&#xff0c;阿里云专家博主&#xff0c;土木转码&#xff0c;现任部门技术 leader&#xff0c;专注于互联网技术分享&#xff0c;职场经验分享。 &#x1f525;热门文章推荐&#…

【教程】R语言生物群落(生态)数据统计分析与绘图

查看原文>>>R语言生物群落&#xff08;生态&#xff09;数据统计分析与绘图实践 暨融合《R语言基础》、《tidyverse数据清洗》、《多元统计分析》、《随机森林模型》、《回归及混合效应模型》、《结构方程模型》、《统计结果作图》七合一版本方案 R 语言作的开源、自…

httpclient工具类(支持泛型转换)

1、网上搜到的httpclient工具类的问题&#xff1a; 1.1、如下图我们都能够发现这种封装的问题&#xff1a; 代码繁杂、充斥了很多重复性代码返回值单一&#xff0c;无法拿到对应的Java Bean对象及List对象集合实际场景中会对接大量第三方的OPEN API&#xff0c;下述方法的扩展…

预处理详解(二)

1.宏和函数对比 宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个。 #define MAX(a, b) ((a)>(b)?(a):(b)) 那为什么不用函数来完成这个任务&#xff1f; 原因有二&#xff1a; 1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所…

Hadoop相关知识点

文章目录 一、主要命令二、配置虚拟机2.1 设置静态ip2.2 修改主机名及映射2.3 修改映射2.4 单机模式2.5 伪分布式2.6 完全分布式 三、初识Hadoop四、三种模式的区别4.1、单机模式与伪分布式模式的区别4.2、特点4.3、配置文件的差异4.3.1、单机模式4.3.2、伪分布式模式4.3.3、完…

ChatGPT 被爆重大隐私泄露!在回答时突然蹦出陌生男子自拍照,你的数据都将被偷走训练模型!

ChatGPT 被爆重大隐私泄露 &#xff01; 一位用户在向 ChatGPT 询问 Python 中的代码格式化包 black 的用法时&#xff0c;没有一点点防备&#xff0c;ChatGPT 在回答中插入了一个陌生男子的自拍照&#xff08;手动捂脸.jpg&#xff09; 可以看到刚开始 ChatGPT 还相当正常&am…

CentOS停更沉寂,RHEL巨变限制源代:Docker容器化技术的兴起助力操作系统新格局

一、概述 操作系统是计算机系统的核心软件&#xff0c;它管理和控制着计算机的硬件和软件资源&#xff0c;为用户和应用程序提供了一个统一、高效、安全的运行环境。操作系统的发展历史也是计算机技术的发展历史的重要组成部分&#xff0c;它见证了计算机从单机到网络&#xf…

vue工程化开发和脚手架

工程化开发和脚手架 1.开发Vue的两种方式 核心包传统开发模式&#xff1a;基于html / css / js 文件&#xff0c;直接引入核心包&#xff0c;开发 Vue。工程化开发模式&#xff1a;基于构建工具&#xff08;例如&#xff1a;webpack&#xff09;的环境中开发Vue。 工程化开…

使用Nokogiri和OpenURI库进行HTTP爬虫

目录 一、Nokogiri库 二、OpenURI库 三、结合Nokogiri和OpenURI进行爬虫编程 四、高级爬虫编程 1、并发爬取 2、错误处理和异常处理 3、深度爬取 总结 在当今的数字化时代&#xff0c;网络爬虫已经成为收集和处理大量信息的重要工具。其中&#xff0c;Nokogiri和OpenUR…

web3 React dapp中编写balance组件从redux取出并展示用户资产

好啊 上文WEB3 在 React搭建的Dapp中通过redux全局获取并存储用户ETH与自定义token与交易所存储数量中 我们拿到了用户的一个本身 和 交易所token数量 并放进了redux中做了一个全局管理 然后 我们继续 先 起来ganache的一个模拟环境 ganache -d然后 我们启动自己的项目 顺手发…

Go语言集成开发环境(IDE):GoLand 2023中文

GoLand 2023是一款由JetBrains开发的现代化、功能丰富的Go语言集成开发环境&#xff08;IDE&#xff09;。它提供了智能代码提示和自动完成、强大的内置调试器以及代码重构工具&#xff0c;帮助开发者提高编码效率并确保代码质量。GoLand 2023还支持多种版本控制系统&#xff0…

python3 阿里云api进行巡检发送邮件

python3 脚本爬取阿里云进行巡检 不确定pip能不能安装上&#xff0c;使用时候可以百度一下&#xff0c;脚本是可以使用的&#xff0c;没有问题的 太长时间了&#xff0c;pip安装依赖忘记那些了&#xff0c;使用科大星火询问了下&#xff0c;给了下面的&#xff0c;看看能不能使…

【MATLAB源码-第67期】基于麻雀搜索算法(SSA)的无人机三维地图路径规划,输出最短路径和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 ​麻雀搜索算法&#xff08;Sparrow Search Algorithm, SSA&#xff09;是一种新颖的元启发式优化算法&#xff0c;它受到麻雀社会行为的启发。这种算法通过模拟麻雀的食物搜索行为和逃避天敌的策略来解决优化问题。SSA通过模…

世微 DC-DC平均电流双路降压恒流驱动器 LED车灯AP2813

产品描述 AP2813 是一款双路降压恒流驱动器,高效率、外 围简单、内置功率管&#xff0c;适用于 5-80V 输入的高精度降 压 LED 恒流驱动芯片。内置功率管输出最大功率可达 12W&#xff0c;最大电流 1.2A。 AP2813 一路直亮&#xff0c;另外一路通过 MODE1 切换 全亮&#xff0c…