JWT
一、生成令牌,解析令牌(对称盐)
package com.fzy.demo; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Calendar; import java.util.Date; public class Demo01 { // public static void main(String[] args) { // // //失效时间 // Calendar instance = Calendar.getInstance(); // instance.add(Calendar.MINUTE,100); // // Date expireTime = instance.getTime(); // // //颁发jwt令牌 // String jwtToken = JWT.create() .withHeader() // .withClaim("id", "1") // .withClaim("username", "jack") // .withClaim("age", 18) // .withExpiresAt(expireTime) // .sign(Algorithm.HMAC256("abc")); // // System.out.println(jwtToken); // } public static void main(String[] args) { //解析令牌 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("abc")).build(); DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleHAiOjE2NTkxNzgxNTcsImFnZSI6MTgsInVzZXJuYW1lIjoiamFjayJ9.whcL4pRCpfy-SrykkdskbjpDbnZSiFzkrHzLHyETQIg"); //获取payload(载荷)信息 String id = verify.getClaim("id").asString(); String username = verify.getClaim("username").asString(); Integer age = verify.getClaim("age").asInt(); System.out.println("id = " + id); } }
二、基于jwt的非对称加密 登录&认证流程:
- 客户端提交登录请求到服务端,服务端校验登录请求,如果用户合法,那么颁发令牌,用户不合法,跳到登录页面。
- 返回jwt令牌,存入到localstorage,
- 访问受限资源,把令牌取出,放入到请求头中,
- 受限资源拿到令牌,解析令牌,如果合法,那么返回结果,不合法,那就拒绝访问
0.前端发送登录请求
<script> //配置请求地址 axios.defaults.baseURL = "http://localhost:8080" //配置拦截器信息 axios.interceptors.response.use(res=>{ return res.data; }) //创建Vue实例,得到 ViewModel var vm = new Vue({ el: '#app', data:{ user:{ account:"", password:"", } }, methods: { login(){ axios.post("/user/login",this.user).then(res=>{ console.log(res); //将令牌存储localstorage localStorage.setItem("token",res.data) }) } }, created(){} }); </script>
1.后端接收登录请求
UserController.java
package com.fzy.controller; import com.fzy.entity.SysUser; import com.fzy.interceptor.AccessInterceptor; import com.fzy.service.UserService; import com.fzy.vo.ResultVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("user") @CrossOrigin //配置跨域 public class UserController { @Autowired private AccessInterceptor accessInterceptor; @Autowired private UserService userService; // 一、用户登录(传入用户信息) @RequestMapping("login") public ResultVo<String> login(@RequestBody SysUser sysUser){ return userService.login(sysUser); } //四、用户访问认证(携带token) @RequestMapping("one") public SysUser one(String userId){ //(略,改为拦截器实现)获取jwt令牌,解析jwt令牌 //七、获取用户信息 SysUser sysUser = accessInterceptor.getLocal().get(); return sysUser; } }
2.验证登录请求,并生成token返回前端
UserServiceImpl.java
package com.fzy.service; import com.fzy.entity.SysUser; import com.fzy.mapper.UserMapper; import com.fzy.util.JwtUtils; import com.fzy.util.RsaUtils; import com.fzy.vo.ResultVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.ResourceUtils; import java.security.PrivateKey; @Service public class UserServiceImpl implements UserService{ @Autowired(required = false) private UserMapper userMapper; @Override public ResultVo<String> login(SysUser sysUser) { // 二、验证登录信息(进行密码比较) if(sysUser == null){ return new ResultVo<String>(false,"非法参数"); } //获取用户信息 SysUser userDB = userMapper.findUserByUsername(sysUser.getAccount()); if(userDB == null){ return new ResultVo<>(false,"用户名或者密码错误"); } //比较密码 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); if(!encoder.matches(sysUser.getPassword(),userDB.getPassword())){ return new ResultVo<>(false,"用户名或者密码错误"); } try { // 三、验证通过,颁发令牌(把私钥存入token返回前端) //加载私钥 PrivateKey privateKey = RsaUtils.getPrivateKey(ResourceUtils.getFile("classpath:pri").getPath()); userDB.setPassword(""); String jwtToken = JwtUtils.generateTokenExpireInMinutes(userDB, privateKey, 30); return new ResultVo<>(true,"success",jwtToken); } catch (Exception e) { e.printStackTrace(); return new ResultVo<>(false,"用户名或者密码错误"); } } }
3.前端登录携带token 进行验证
AccessInterceptor.java (在拦截器配置 验证流程)
package com.fzy.interceptor; import cn.hutool.json.JSONUtil; import com.fzy.entity.SysUser; import com.fzy.exception.JWTException; import com.fzy.util.JwtUtils; import com.fzy.util.RsaUtils; import com.fzy.vo.ResultVo; import org.springframework.stereotype.Component; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.PublicKey; @Component public class AccessInterceptor implements HandlerInterceptor { private ThreadLocal<SysUser> local = new ThreadLocal<>(); public ThreadLocal<SysUser> getLocal() { return local; } public void setLocal(ThreadLocal<SysUser> local) { this.local = local; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 五、全局校验jwt令牌(从请求头中获取token) String token = request.getHeader("token"); if (StringUtils.isEmpty(token)) { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); ResultVo<Object> resultVo = new ResultVo<>(false,"令牌不能为空"); response.getWriter().write(JSONUtil.toJsonStr(resultVo)); return false; } // 六,校验token合法,加载公钥,生成用户信息(把用户信息放入threadLocal中,或者放到上下文中) //合法 //加载公钥 PublicKey publicKey = RsaUtils.getPublicKey(ResourceUtils.getFile("classpath:pub").getPath()); try { SysUser infoFromToken = (SysUser) JwtUtils.getInfoFromToken(token, publicKey, SysUser.class); //threadLocal local.set(infoFromToken); return true; } catch (JWTException e) { e.printStackTrace(); response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); ResultVo<Object> resultVo = new ResultVo<>(false,"invalid token"); response.getWriter().write(JSONUtil.toJsonStr(resultVo)); return false; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 八、清空 threadLocal local.remove(); } }
4.配置登录放过页
AccessInterceptorConfig.java
package com.fzy.interceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class AccessInterceptorConfig implements WebMvcConfigurer { @Autowired private AccessInterceptor accessInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accessInterceptor) .excludePathPatterns("/user/login"); } }
5.(略)工具类,以及依赖
JwtUtils.java (生成token以及校验token相关方法)
package com.fzy.util; import cn.hutool.json.JSONUtil; import com.fzy.exception.JWTException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.security.PrivateKey; import java.security.PublicKey; import java.util.*; /** * 生成token以及校验token相关方法 */ public class JwtUtils { private static final String JWT_PAYLOAD_USER_KEY = "user"; /** * 私钥加密token * @param userInfo 载荷中的数据 * @param privateKey 私钥 * @param expire 过期时间,单位分钟 * @return JWT */ public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) { //计算过期时间 Calendar c = Calendar.getInstance(); c.add(Calendar.MINUTE,expire); return Jwts.builder() .claim(JWT_PAYLOAD_USER_KEY, JSONUtil.toJsonStr(userInfo)) .setId(new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()))) .setExpiration(c.getTime()) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } /** * 私钥加密token * @param userInfo 载荷中的数据 * @param privateKey 私钥 * @param expire 过期时间,单位秒 * @return JWT */ public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) { //计算过期时间 Calendar c = Calendar.getInstance(); c.add(Calendar.SECOND,expire); return Jwts.builder() //{"user":"{id:12,name:jack}"} .claim(JWT_PAYLOAD_USER_KEY, JSONUtil.toJsonStr(userInfo)) .setId(new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()))) .setExpiration(c.getTime()) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 */ public static Object getInfoFromToken(String token, PublicKey publicKey, Class userType) throws JWTException { //解析token try { Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); Claims body = claimsJws.getBody(); String userInfoJson = body.get(JWT_PAYLOAD_USER_KEY).toString(); return JSONUtil.toBean(userInfoJson, userType); }catch (Exception e){ throw new JWTException("令牌不合法"); } } }
RsaUtils.java (获取,读取公钥,私钥方法)
package com.fzy.util; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class RsaUtils { private static final int DEFAULT_KEY_SIZE = 2048; /** * @param filename 公钥保存路径,相对于classpath * @return 公钥对象 * @throws Exception */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /** * 从文件中读取密钥 * * @param filename 私钥保存路径,相对于classpath * @return 私钥对象 * @throws Exception */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * 获取公钥 * @param bytes 公钥的字节形式 * @return * @throws Exception */ public static PublicKey getPublicKey(byte[] bytes) throws Exception { bytes = Base64.getDecoder().decode(bytes); X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } /** * 获取密钥 * @param bytes 私钥的字节形式 * @return * @throws Exception */ public static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { bytes = Base64.getDecoder().decode(bytes); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(spec); } /** * 根据密文,生存rsa公钥和私钥,并写入指定文件 * * @param publicKeyFilename 公钥文件路径 * @param privateKeyFilename 私钥文件路径 * @param secret 生成密钥的密文 */ public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(secret.getBytes()); keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom); KeyPair keyPair = keyPairGenerator.genKeyPair(); // 获取公钥并写出 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes); writeFile(publicKeyFilename, publicKeyBytes); // 获取私钥并写出 byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes); writeFile(privateKeyFilename, privateKeyBytes); } private static byte[] readFile(String fileName) throws Exception { return Files.readAllBytes(new File(fileName).toPath()); } private static void writeFile(String destPath, byte[] bytes) throws IOException { File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } Files.write(dest.toPath(), bytes); } }
pom.xml(依赖)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.fzy</groupId> <artifactId>login</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <mysql.version>5.1.47</mysql.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.7.RELEASE</version> </parent> <dependencies> <!-- jwt,对称盐--> <!-- <dependency>--> <!-- <groupId>com.auth0</groupId>--> <!-- <artifactId>java-jwt</artifactId>--> <!-- <version>3.4.0</version>--> <!-- </dependency>--> <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> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.7</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.16</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
pri (私钥)
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqvzbDiomsr8R6wxUIlbcE6uZOAmYt6iX5Qo9EJxuNfg1nttLcshOGxztKrUsY/uovkCj+snktJnxtIHwPV6USSMO/ZsH2FM+a/yYlxs0vJS+OClZzTMgw70w8e+TmsQRBAwFGBjaigmtevFneT32MVj08tLVjCZmDW+BM8vCwgtB8lXG52Gb4C3iYC7ow/6n6Nl17OuYF//8HLGyCytyqeb/LPrSZednzopoS5QtNW2nGyHoeIB/CiVBiXFPqvQI1PNL/tDan++jFrtPnLxS6d6+0WpZ4Xv/c/TG4AdvVozCkd5vsTBpj06h9YMSEhaSqNpAT7LlppLiKQig8AEvJAgMBAAECggEBAIuAPhX0khbsY/KIgB7LcwQphpAllaXdr7i+kJ519STc38bkr9JwC1QKYn7Ypop9eofxmAy/dBFXaEEgUTuXt94AZS1znvSWfCd+XoRusCTclMXP3GHHUafpWrfv4Uw5Q+h5tAip+uk69M4pxKLE/yYxL0bxvWB0719LKS61LcCHSm/0tcSy9Pl6INti+55Pt0ervK24mfu5wLbdid6sA7BjGFaIzp6fuZiJnaSIVhG7POcwsYO1413VTuqw6LfVTgKR8QGR4RVMdvBiQcPWKFVtsRRWR7G9k4xPxiq7ruhPLYtkR17mL+0tUXBSzxq4cZsHBcwK0ZWzN3Shqr/uiyECgYEA1McXlNff1e/BYpPCynvn83Hg/5XCUcht8pqDAAdK08mtEF6UdUqCuFA3cDOpvs+eKkJ5IvhR8FSxUS0UbChrmknu7OMNxPstfMwfTZBiKJh6O6h9l3bdQhfTUg7yXRrGraZbeX7OVm2BClO6eSch7Gkyq3nFeNuulZSJGGMaFRcCgYEAzW5t4rphPu3TT9xpQu5MDI6rGZ12GCPtBX+adwTcs1UezV2W6C4eEVfSyeBGJifnBb/WM+Y9EsNJ4ST82Dn4VaMDmjWTHZubJeEwu9H2XUGEP+dDGfilZvlvF6US5C9qO/O8WJKuTxh9/6ujKSTSUJ79YwALpdXO6CI9QMxY8h8CgYBT77oIuGQPZAYYmguEjv6AVZMZn/1I/9UHmBZHY5kMFQnVZEoSPvN0PAiylV7H6+UL7K8WsXi5Yt+UE0F0LTnNYo8DGcqrwx49ldRfZ66hLw1BDYWFw5ki4n01aLoJKm/nvrFlKzXAeHAoH8F2244RUapwZJgWxiqHzVGLskRuvwKBgGpqgvfJLqjnj+g1uD1YrgJvQUjinZNBwP1xLXXsCdvIA03E5nBdu2umf+XdjNklIHKE/pQh948ppLLBc00bMn5CFJKkoBHdUpBbx9/zktiaIMTtqn/gouXl3lt0QoX73d8ykmWbjjog6NwxcDLXBS+IPKA1HNOKH0V2vx46/PRnAoGBAMX36M8caj/nijh07a5Pv6l9mG2eXEgtlKWy3UQLVJuPZR/6dgrMipv6PgmzrNawP+4QPfcnxWltggoXRudJZDm7iEwW7y0NUtm1ndfDU0AQf1FpwoRsZpp6cVEjP5SbtJSsy5+YZqRdL3FKMZovnJLSV5cs5ovZc8A6SAD56vYr
pub(公钥)
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqr82w4qJrK/EesMVCJW3BOrmTgJmLeol+UKPRCcbjX4NZ7bS3LIThsc7Sq1LGP7qL5Ao/rJ5LSZ8bSB8D1elEkjDv2bB9hTPmv8mJcbNLyUvjgpWc0zIMO9MPHvk5rEEQQMBRgY2ooJrXrxZ3k99jFY9PLS1YwmZg1vgTPLwsILQfJVxudhm+At4mAu6MP+p+jZdezrmBf//Byxsgsrcqnm/yz60mXnZ86KaEuULTVtpxsh6HiAfwolQYlxT6r0CNTzS/7Q2p/voxa7T5y8UunevtFqWeF7/3P0xuAHb1aMwpHeb7EwaY9OofWDEhIWkqjaQE+y5aaS4ikIoPABLyQIDAQAB