2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > Spring Boot整合JWT实现用户认证

Spring Boot整合JWT实现用户认证

时间:2022-07-06 16:52:08

相关推荐

Spring Boot整合JWT实现用户认证

JWT实现用户认证

在介绍完JWT之后我们使用springboot整合JWT实现用户认证。

前后端分离使用JWT做用户认证(概述)

JWT实现认证的原理

​服务器在生成一个JWT之后会将这个JWT会以Authorization : Bearer JWT 键值对的形式存放在 cookies里面发送到客户端机器,在客户端再次访问收到JWT保护的资源URL链接的时候,服务器会获取到cookies中存放的JWT信息,首先将Header进行反编码获取到加密的算法,在通过存放在服务器上的密匙对Header.Payload 这个字符串进行加密,比对JWT中的Signature和实际加密出来的结果是否一致,如果一致那么说明该JWT是合法有效的,认证成功,否则认证失败。

JWT实现用户认证的流程图

JWT的代码实现

代码说明:

代码中与JWT有关的内容如下

config包中MvcConfig类配置生成一个JWT并配置了JWT拦截的URL,JwtProperties用于从配置文件中读取数据web包中UserController用于处理用户的登录,校验时生成JWTutils包中JwtUtils 用于对JWT的加密解析,RsaUtils用于从文件中读取公私钥interceptor包中LoginInterceptor实现对登录的拦截认证constants包中对JWT加密时要包含的内容

其余的是属于对数据库访问的相关内容,以及异常提示内容和捕捉异常信息内容。(在下面贴出代码)

引入关键依赖pom.xml

<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>

application.yml文件

server:port: 8888spring:datasource:url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=GMT%2B8username: rootpassword: sasadriver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: update show-sql: truejwt:secret: demo@Login(Auth}*^31)&demo% # 登录校验的密钥pubKeyPath: D:/coding/rsa/rsa.pub # 公钥地址priKeyPath: D:/coding/rsa/rsa.pri # 私钥地址expire: 30 # 过期时间,单位分钟cookieName: DB_TOKEN

JwtUtils类加密解密token封装的方法

package com.demo.ssodemo.utils;import com.demo.ssodemo.constants.JwtConstans;import com.demo.ssodemo.constants.UserInfo;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jws;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.joda.time.DateTime;import java.security.PrivateKey;import java.security.PublicKey;public class JwtUtils {/*** 私钥加密token** @param userInfo载荷中的数据* @param privateKey 私钥* @param expireMinutes 过期时间,单位秒* @return* @throws Exception*/public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) throws Exception {//JWT_KEY_ID为写入token中用户idreturn Jwts.builder().claim(JwtConstans.JWT_KEY_ID, userInfo.getId()).claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()).setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()).signWith(SignatureAlgorithm.RS256, privateKey).compact();}/*** 私钥加密token** @param userInfo载荷中的数据* @param privateKey 私钥字节数组* @param expireMinutes 过期时间,单位秒* @return* @throws Exception*/public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {return Jwts.builder().claim(JwtConstans.JWT_KEY_ID, userInfo.getId()).claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()).setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()).signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)).compact();}/*** 公钥解析token** @param token用户请求中的token* @param publicKey 公钥* @return* @throws Exception*/private static Jws<Claims> parserToken(String token, PublicKey publicKey) {return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);}/*** 公钥解析token** @param token用户请求中的token* @param publicKey 公钥字节数组* @return* @throws Exception*/private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)).parseClaimsJws(token);}/*** 获取token中的用户信息** @param token用户请求中的令牌* @param publicKey 公钥* @return 用户信息* @throws Exception*/public static UserInfo getInfoFromToken(String token, PublicKey publicKey) throws Exception {Jws<Claims> claimsJws = parserToken(token, publicKey);Claims body = claimsJws.getBody();return new UserInfo(ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)));}/*** 获取token中的用户信息** @param token用户请求中的令牌* @param publicKey 公钥字节数组* @return 用户信息* @throws Exception*/public static UserInfo getInfoFromToken(String token, byte[] publicKey) throws Exception {Jws<Claims> claimsJws = parserToken(token, publicKey);Claims body = claimsJws.getBody();return new UserInfo(ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)));}}

RsaUtils工具类

package com.demo.ssodemo.utils;import java.io.File;import java.io.IOException;import java.nio.file.Files;import java.security.*;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;public class RsaUtils {/*** 从文件中读取公钥** @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 {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 Exception {PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);KeyFactory factory = KeyFactory.getInstance("RSA");return factory.generatePrivate(spec);}/*** 根据密文,生成rsa公钥和私钥,并写入指定文件** @param publicKeyFilename 公钥文件路径* @param privateKeyFilename 私钥文件路径* @param secret 生成密钥的密文* @throws IOException* @throws NoSuchAlgorithmException*/public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");SecureRandom secureRandom = new SecureRandom(secret.getBytes());keyPairGenerator.initialize(1024, secureRandom);KeyPair keyPair = keyPairGenerator.genKeyPair();// 获取公钥并写出byte[] publicKeyBytes = keyPair.getPublic().getEncoded();writeFile(publicKeyFilename, publicKeyBytes);// 获取私钥并写出byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();writeFile(privateKeyFilename, privateKeyBytes);}/*** 读取文件* @param fileName* @return* @throws Exception*/private static byte[] readFile(String fileName) throws Exception {return Files.readAllBytes(new File(fileName).toPath());}/*** 把二进制写入文件* @param destPath* @param bytes* @throws IOException*/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);}}

MvcConfig类,配置要的拦截或放行的URL,配合LoginInterceptor一起使用

package com.demo.ssodemo.config;import com.demo.ssodemo.interceptor.LoginInterceptor;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module.SimpleModule;import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.StringHttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.math.BigInteger;import java.nio.charset.Charset;import java.util.ArrayList;import java.util.List;@Configuration@EnableWebMvcpublic class MvcConfig implements WebMvcConfigurer {@Autowiredprivate JwtProperties jwtProperties;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Beanpublic LoginInterceptor loginInterceptor() {return new LoginInterceptor(jwtProperties,stringRedisTemplate);}@Overridepublic void addInterceptors(InterceptorRegistry registry) {//配置放行的路径List<String> excludePath = new ArrayList<>();excludePath.add("/swagger-ui.html");excludePath.add("/swagger-resources/**");excludePath.add("/webjars/**");excludePath.add("/login/**");excludePath.add("/login");//需要测试,暂时放行registry.addInterceptor(loginInterceptor())//注册拦截器//拦截所有.addPathPatterns("/**").excludePathPatterns(excludePath);}@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {MappingJackson2HttpMessageConverter jackson2HttpMessageConverter =new MappingJackson2HttpMessageConverter();ObjectMapper objectMapper = new ObjectMapper();SimpleModule simpleModule = new SimpleModule();simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);simpleModule.addSerializer(Long.class, ToStringSerializer.instance);simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);objectMapper.registerModule(simpleModule);jackson2HttpMessageConverter.setObjectMapper(objectMapper);converters.add(jackson2HttpMessageConverter);converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));}}

LoginInterceptor,登录拦截器,拦截请求,如token失效或没有登录直接拦截回去

package com.demo.ssodemo.interceptor;import com.demo.ssodemo.config.JwtProperties;import com.demo.ssodemo.constants.UserInfo;import com.demo.ssodemo.utils.CookieUtils;import com.demo.ssodemo.utils.JwtUtils;import mons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.http.HttpStatus;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/*** @Feature: 登录拦截器*/public class LoginInterceptor extends HandlerInterceptorAdapter {private JwtProperties jwtProperties;private StringRedisTemplate stringRedisTemplate;/*** 定义一个线程域,存放登录用户*/private static final ThreadLocal<UserInfo> t1 = new ThreadLocal<>();public LoginInterceptor(JwtProperties jwtProperties,StringRedisTemplate stringRedisTemplate) {this.jwtProperties = jwtProperties;this.stringRedisTemplate = stringRedisTemplate;}/*** 在业务处理器处理请求之前被调用* 如果返回false* 则从当前的拦截器往回执行所有拦截器的afterCompletion(),再退出拦截器链* 如果返回true* 执行下一个拦截器,直到所有拦截器都执行完毕* 再执行被拦截的Controller* 然后进入拦截器链* 从最后一个拦截器往回执行所有的postHandle()* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.查询tokenString token = CookieUtils.getCookieValue(request,jwtProperties.getCookieName());if (StringUtils.isBlank(token)){//2.未登录,返回401response.setStatus(HttpStatus.UNAUTHORIZED.value());return false;}//3.有token,查询用户信息try{//4.解析成功,说明已经登录UserInfo userInfo = JwtUtils.getInfoFromToken(token,jwtProperties.getPublicKey());//5.放入线程域t1.set(userInfo);return true;}catch (Exception e){//6.抛出异常,证明未登录,返回401response.setStatus(HttpStatus.UNAUTHORIZED.value());return false;}}/*** 在业务处理器处理请求执行完成后,生成视图之前执行的动作* 可在modelAndView中加入数据,比如当前时间* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}/*** 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等* 当有拦截器抛出异常时,会从当前拦截器往回执行所有的拦截器的afterCompletion()* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {t1.remove();}//返回用户信息public static UserInfo getLoginUser(){return t1.get();}}

JwtProperties,用于从配置文件中读取数据

package com.demo.ssodemo.config;import com.demo.ssodemo.utils.RsaUtils;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;import java.io.File;import java.security.PrivateKey;import java.security.PublicKey;@Data//@ConfigurationProperties(prefix = "jwt")@Configurationpublic class JwtProperties {@Value("${jwt.secret}")private String secret; // 密钥@Value("${jwt.pubKeyPath}")private String pubKeyPath;// 公钥@Value("${jwt.priKeyPath}")private String priKeyPath;// 私钥@Value("${jwt.expire}")private int expire;// token过期时间@Value("${jwt.cookieName}")private String cookieName;private PublicKey publicKey;private PrivateKey privateKey;//对象一旦实例化后,就应该读取公钥和私钥@PostConstruct //构造函数执行完成后执行public void init() throws Exception {//公钥私钥不存在,先生成File pubkeyPath = new File(pubKeyPath);File prikeyPath = new File(priKeyPath);if (!pubkeyPath.exists() || !prikeyPath.exists()) {RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);}//读取公钥和私钥this.publicKey = RsaUtils.getPublicKey(pubKeyPath);this.privateKey = RsaUtils.getPrivateKey(priKeyPath);}}

constants包下JwtConstans为加密token时需要写入的用户的基本信息,UserInfo用于解密后存放用户信息

JwtConstans

public abstract class JwtConstans {public static final String JWT_KEY_ID = "id";public static final String JWT_KEY_USER_NAME = "username";}

UserInfo

@Data@AllArgsConstructor@NoArgsConstructorpublic class UserInfo {private Long id;private String username;}

我们需要使用到的工具类和拦截器以完成,我们编写Controller,service来测试

Controller

package com.demo.ssodemo.web;import com.demo.ssodemo.config.JwtProperties;import com.demo.ssodemo.constants.UserInfo;import com.demo.ssodemo.enums.ExceptionEnum;import com.demo.ssodemo.exception.CustomException;import com.demo.ssodemo.interceptor.LoginInterceptor;import com.demo.ssodemo.service.UserService;import com.demo.ssodemo.utils.CookieUtils;import com.demo.ssodemo.utils.JwtUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@RestControllerpublic class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtProperties prop;@PostMapping("/login")public ResponseEntity<Void> login(@RequestParam("username") String username,@RequestParam("password") String password,HttpServletResponse response, HttpServletRequest request) {//登录String token = userService.login(username, password);CookieUtils.newBuilder(response).httpOnly().request(request).build(prop.getCookieName(), token);return ResponseEntity.status(HttpStatus.OK).build();}/*** 校验用户登录状态** @return*/@GetMapping("verify")public ResponseEntity<UserInfo> verify(@CookieValue("DB_TOKEN") String token,HttpServletResponse response, HttpServletRequest request) {try {//解析token//已经拦截解析,取值即可UserInfo info = LoginInterceptor.getLoginUser();//刷新token,重新生成tokenString newToken = JwtUtils.generateToken(info, prop.getPrivateKey(), prop.getExpire());//写入token中CookieUtils.newBuilder(response).httpOnly().request(request).build(prop.getCookieName(),newToken);//已登录,返回用户信息return ResponseEntity.ok(info);} catch (Exception e) {//token已过期,或者token无效throw new CustomException(ExceptionEnum.UNAUTHORIZED);}}}

UserServiceImpl 实现

package com.demo.ssodemo.service.impl;import com.demo.ssodemo.config.JwtProperties;import com.demo.ssodemo.constants.UserInfo;import com.demo.ssodemo.enums.ExceptionEnum;import com.demo.ssodemo.exception.CustomException;import com.demo.ssodemo.pojo.User;import com.demo.ssodemo.repository.UserRepository;import com.demo.ssodemo.service.UserService;import com.demo.ssodemo.utils.CodecUtils;import com.demo.ssodemo.utils.JwtUtils;import lombok.extern.slf4j.Slf4j;import mons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Example;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.Optional;@Slf4j@Servicepublic class UserServiceImpl implements UserService {@Autowiredprivate UserRepository goodsRepository;@Autowiredprivate JwtProperties prop;private User queruUserByUsernameAndPassword(String username, String password) {//查询用户User record = new User();record.setUsername(username);Optional<User> user = goodsRepository.findOne(Example.of(record));//校验if (!user.isPresent()) {throw new CustomException(ExceptionEnum.INVALID_USERNAME_PASSWORD);}//校验密码String pwd = CodecUtils.md5Hex(password, user.get().getSalt());if (!StringUtils.equals(user.get().getPassword(), pwd)) {throw new CustomException(ExceptionEnum.INVALID_USERNAME_PASSWORD);}return user.get();}@Overridepublic String login(String username, String password) {try {//根据用户名和密码查询User user = queruUserByUsernameAndPassword(username, password);//判断userif (user == null) {throw new CustomException(ExceptionEnum.INVALID_USERNAME_PASSWORD);}//jwtUtils生成jwt类型的token//生成tokenString token = JwtUtils.generateToken(new UserInfo(user.getId(), username), prop.getPrivateKey(), prop.getExpire());return token;} catch (Exception e) {log.error("[登陆中心] 用户名或密码有误,用户名称{}", username, e);throw new CustomException(ExceptionEnum.INVALID_USERNAME_PASSWORD);}}}

接下来我们在Postman工具中测试,可以看到成功写入cookie中,然后我们每次请求都会携带这个token,我们只需要拦截下来校验即可,就可以实现单点登录一样的效果。

我们请求校验用户登录状态的方法,或请求我们网站其他页面时,看看我们的拦截器是否有效果,是否能成功解析出用户信息。

可以看到我们的请求已经被拦截,并且我们使用工具类获取到客户端携带的cookie中我们设置的token,并成功解析,放入线程域中,以便我们在校验用户状态的方法中获取用户信息,在获取信息后我们刷新一遍token并重新写入cookie中,最后返回用户信息。

总结:

​ 在我们登录一个网站之后,下一次打开网页的时候可能显示的还是登录的状态,不需要再次进行登录操作,通过JWT就可以实现这样一个用户认证的功能。当然使用Session可以实现这个功能,但是使用Session的同时也会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端机器上,从而减轻服务器的压力。

​ 其实业务系统代码非常简单,主要是用了一个拦截器,拦截 http 请求,提取出 token 向 sso 认证中心验证 token 是否有效,有效放行,否则返回错误给前端。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。