spring boot:在服务端用redis存储jwt登录后的用户信息(spring boot 2.4.4)
一,用redis存储用户信息的好处?
1,避免解析token之后需要查库得到用户的信息
2, 因为jwt的token可以被反解,所以不直接使用username生成token,而是用一个随机的字符串代替
避免安全问题
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/27/spring-boot-zai-fu-wu-duan-yong-redis-cun-chu-jwt-deng-lu/
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔 邮箱: 371125307@qq.com
二,演示项目的相关信息
1,地址:
https://github.com/liuhongdi/jwtredis
2,功能说明:演示了用redis来存储jwt登录后的用户信息
3,项目结构:如图:
三,配置文件说明
1,pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--security begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jjwt begin--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--thymeleaf begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--fastjson begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.73</version> </dependency> <!--redis begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.11.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.1</version> </dependency> <!--redis end--> <!--jaxb--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!--mysql mybatis begin--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2,application.yml
spring: thymeleaf: cache: false encoding: UTF-8 mode: HTML prefix: classpath:/templates/ suffix: .html #mysql datasource: url: jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false username: root password: lhddemo driver-class-name: com.mysql.cj.jdbc.Driver #redis1 redis1: enabled: 1 host: 127.0.0.1 port: 6379 password: lhddemo database: 0 lettuce: pool: max-active: 32 max-wait: 300 max-idle: 16 min-idle: 8 #mybatis mybatis: mapper-locations: classpath:/mapper/*Mapper.xml type-aliases-package: com.example.demo.mapper
3,sql:
建表:
CREATE TABLE `sys_user` ( `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码', `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称', PRIMARY KEY (`userId`), UNIQUE KEY `userName` (`userName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'
测试数据:
INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES (1, 'lhd', '$2a$10$yGcOz3cdNI6Ya67tqQueS.raxzTOedGsv5jh2BwtRrI5/K9QEIPGq', '老刘');
四,java代码说明
1,controller/HomeController.java
@Controller @RequestMapping("/home") public class HomeController { //session详情 @GetMapping("/session") @ResponseBody public RestResult session() { System.out.println("-------begin get session:"); if ( SessionUtil.getCurrentUser() == null) { return RestResult.error(ResponseCode.LOGIN_NEED); } else { Map<String, String> data = new HashMap<String, String>(); data.put("username", SessionUtil.getCurrentUser().getUsername()); data.put("userid", String.valueOf(SessionUtil.getCurrentUser().getUserid())); data.put("nickname", SessionUtil.getCurrentUser().getNickname()); data.put("roles", SessionUtil.getCurrentUser().getAuthorities().toString()); return RestResult.success(data); } } //显示getsession页面 @GetMapping("/getsession") public String get() { return "home/getsession"; } //显示login页面 @GetMapping("/login") public String login() { return "home/login"; } }
2,result/RestResult.java
public class RestResult implements Serializable { //uuid,用作唯一标识符,供序列化和反序列化时检测是否一致 private static final long serialVersionUID = 7498483649536881777L; //标识代码,0表示成功,非0表示出错 private Integer code; //提示信息,通常供报错时使用 private String msg; //正常返回时返回的数据 private Object data; public RestResult(Integer status, String msg, Object data) { this.code = status; this.msg = msg; this.data = data; } //返回成功数据 public static RestResult success(Object data) { return new RestResult(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data); } public static RestResult success(Integer code,String msg) { return new RestResult(code, msg, null); } //返回出错数据 public static RestResult error(ResponseCode code) { return new RestResult(code.getCode(), code.getMsg(), null); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
3,jwt/JwtAuthticationFilter.java
@Component public class JwtAuthticationFilter implements Filter { @Resource private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private JwtUserDetailsService userDetailsService; @Resource private UserRedisService userRedisService; @Resource private SysUserService sysUserService; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("----------------AuthticationFilter init"); } //过滤功能 @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //得到当前的url HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String path = request.getServletPath(); if (path.equals("/auth/authenticate")) { System.out.println("auth path:"+path); //得到请求的post参数 String username = ""; String password = ""; try { BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream())); StringBuffer sb=new StringBuffer(); String s=null; while((s=br.readLine())!=null){ sb.append(s); } JSONObject jsonObject = JSONObject.parseObject(sb.toString()); username = jsonObject.getString("username"); password = jsonObject.getString("password"); //System.out.println("name:"+name+" age:"+age); } catch (IOException e) { e.printStackTrace(); } System.out.println("username:"+username); System.out.println("password:"+password); String authResult = ""; try{ authResult = authenticate(username,password); } catch (Exception e) { e.printStackTrace(); } System.out.println("authResult:"+authResult); if ("success".equals(authResult)) { SysUser oneUser = sysUserService.getOneUserByUsername(username);//数据库查询 看用户是否存在 final UserDetails userDetails = userDetailsService.loadUserBySysUser(oneUser); String origToken = jwtTokenUtil.makeTokenForSave(userDetails.getUsername()); final String token = jwtTokenUtil.generateTokenByOrig(origToken); //保存到redis oneUser.setOrigToken(origToken); oneUser.setPassword(""); //System.out.println("保存到redis的token:"+origToken); userRedisService.setOneUser(oneUser,origToken); //return result Map<String, String> mapData = new HashMap<String, String>(); mapData.put("token", token); ServletUtil.printRestResult(RestResult.success(mapData)); } else if ("badcredential".equals(authResult)){ ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL)); } else { ServletUtil.printRestResult(RestResult.error(ResponseCode.ERROR)); } return; } else { System.out.println("not auth path:"+path); filterChain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { System.out.println("----------------filter destroy"); } private String authenticate(String username, String password) throws Exception { try { System.out.println("username:"+username); System.out.println("password:"+password); authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); System.out.println("authenticate:will return success"); return "success"; } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { System.out.println("BadCredentialsException"); System.out.println(e.toString()); //throw new Exception("INVALID_CREDENTIALS", e); return "badcredential"; } } }
4,jwt/JwtRequestFilter.java
@Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Resource private UserRedisService userRedisService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; // JWT Token 获取请求头部的 Bearer System.out.println("filter:header:"+requestTokenHeader); // only the Token if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { //System.out.println("filter :requestTokenHeader not null and start with bearer"); jwtToken = requestTokenHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } catch (MalformedJwtException e) { System.out.println("JWT Token MalformedJwtException"); } } else { //System.out.println("filter :requestTokenHeader is null || not start with bearer"); //logger.warn("JWT Token does not begin with Bearer String"); } // 验证, if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { System.out.println("-----------get username from client:"+username); SysUser oneUser = userRedisService.getOneUserByUserToken(username); if (oneUser == null) { ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_NEED)); return; } //get UserDetails UserDetails userDetails = this.jwtUserDetailsService.loadUserBySysUser(oneUser); // JWT 验证通过 使用Spring Security 管理 if (jwtTokenUtil.validateTokenByOrigToken(jwtToken, oneUser.getOrigToken())) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } else { System.out.println("jwtTokenUtil.validateToken not success"); } } chain.doFilter(request, response); } }
5,jwt/JwtTokenUtil.java
@Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -2550185165626007488L; public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; private String secret = "liuhongdi"; //retrieve username from jwt token public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } //retrieve expiration date from jwt token public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } //for retrieveing any information from token we will need the secret key private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } //check if the token has expired private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } //make a save token public String makeTokenForSave(String userName) { //得到当前时间 long time = System.nanoTime(); //得到随机数 Random ran = new Random(); int x = ran.nextInt(9000) + 1000; //md5后返回 String base = time+"_"+userName+"_"+x; String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } //generate token for user public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); //String token = makeTokenForSave(userDetails.getUsername()); return doGenerateToken(claims, userDetails.getUsername()); } //generate token for user public String generateTokenByOrig(String origToken) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, origToken); } //generate token private String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) .signWith(SignatureAlgorithm.HS512, secret).compact(); } //validate token public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } //validate token by orig public Boolean validateTokenByOrigToken(String token, String origToken) { final String username = getUsernameFromToken(token); //System.out.println("username for valid:"+username); return (username.equals(origToken) && !isTokenExpired(token)); } }
6,jwt/JwtUserDetailsService.java
@Service public class JwtUserDetailsService implements UserDetailsService { @Resource private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("-----loadUserByUsername"); SysUser oneUser = sysUserService.getOneUserByUsername(username);//数据库查询 看用户是否存在 String encodedPassword = oneUser.getPassword(); Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合 //用户角色role前面要添加ROLE_ List<String> roles = oneUser.getRoles(); System.out.println(roles); for (String roleone : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone); collection.add(grantedAuthority); } //给用户增加用户id和昵称 SecUser user = new SecUser(username,encodedPassword,collection); user.setUserid(oneUser.getUserId()); user.setNickname(oneUser.getNickName()); return user; } public UserDetails loadUserBySysUser(SysUser oneUser) throws UsernameNotFoundException { System.out.println("-----loadUserByUser"); //SysUser oneUser = sysUserService.getOneUserByUsername(username);//数据库查询 看用户是否存在 String encodedPassword = oneUser.getPassword(); Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合 //用户角色role前面要添加ROLE_ List<String> roles = oneUser.getRoles(); System.out.println(roles); for (String roleone : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone); collection.add(grantedAuthority); } //给用户增加用户id和昵称 SecUser user = new SecUser(oneUser.getUserName(),encodedPassword,collection); user.setUserid(oneUser.getUserId()); user.setNickname(oneUser.getNickName()); return user; } }
7,impl/UserRedisServiceImpl.java
@Service public class UserRedisServiceImpl implements UserRedisService { @Resource private RedisTemplate redis1Template; //从redis查询查询得到用户信息 @Override public SysUser getOneUserByUserToken(String userToken){ System.out.println("从redis查询得到用户信息"); SysUser userOne; Object usersr = redis1Template.opsForValue().get("jwt_"+userToken); if (usersr == null) { userOne = null; } else { if (usersr.equals("-1")) { userOne = null; } else { userOne = (SysUser)usersr; } } return userOne; } //向redis写入用户信息,保存时长是jwt的配置 @Override public void setOneUser(SysUser user,String userToken){ long timeLenghth = JwtTokenUtil.JWT_TOKEN_VALIDITY; redis1Template.opsForValue().set("jwt_"+userToken,user,timeLenghth, TimeUnit.SECONDS); } }
8,impl/SysUserServiceImpl.java
@Service public class SysUserServiceImpl implements SysUserService { @Resource private UserMapper userMapper; //根据用户名查询数据库得到用户信息 @Override public SysUser getOneUserByUsername(String username) { System.out.println("从数据库查询得到用户信息"); SysUser user_one = userMapper.selectOneUserByUserName(username); return user_one; } }
9,其他相关代码可访问github
五,测试效果
1,访问login
http://127.0.0.1:8080/home/login
登录:
2,访问session:
http://127.0.0.1:8080/home/getsession
点击 get session info按钮:
3,从redis进行查询:
[root@localhost liuhongdi]# /usr/local/soft/redis/bin/redis-cli 127.0.0.1:6379> get jwt_73179e0988afc51896f7810df9716e52 "{\"@class\":\"com.jwtredis.demo.pojo.SysUser\",\"userId\":1,\"userName\":\"lhd\",\"password\":\"\",
\"roles\":[\"java.util.ArrayList\",[]],\"nickName\":\"\xe8\x80\x81\xe5\x88\x98\",
\"origToken\":\"73179e0988afc51896f7810df9716e52\"}"
六,查看spring boot的版本
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.4)