Springboot+Jwt+Redis 完成Token登录认证

前言

本章内容建立在Springboot 基础框架搭建好的基础上进行整合jwt+redis 实现token登录认证

大致流程

  1. 前端 :前端拿到登录名和密码,使用JSEncrypt实现rsa将密码进行加密(这个部分网上有很多,不了解的自行去查询,有时间本人也会更新),然后传到后端;
  2. 后端 :拿到密码通过后台的私钥进行解密,然后通过用户名查询到用户信息(用户名唯一),通过用户信息的状态来判断登录结果。
  3. 前端 :若后端验证成功则根据规则生成Token,并存入redis,且向前端返回token,前端则将token和用户存到localstorage,客户端再次发送请求数据将携带token。
  4. 后端 :通过拦截器拦截请求,在请求头中拿到token,验证redis中是否有token,且是否过期,如果token不存在或者过期,则提示身份验证未通过,或者验证信息失效,否则则请求成功

注:在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同

知识点

一、Token

Token产生的背景:客户端频繁的向服务器请求数据,服务器频繁的去数据库查询用户名和密码进行比对,判断用户名和密码正确与否,并作出相应的提示,这样的情况下服务器压力较大,而Token的目的就是为了减轻服务器的压力,减少频繁的查询数据库,使得服务器更加的健壮。
2.Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。(换一种说法,甚至可以理解为洗浴会所的手牌,第一次进入会所,前台通过一定操作,会给你一个手牌,这个手牌即标志着你的身份,你随后的消费都可以通过这个手牌进行)

二、JWT

1.JWT全称为json web token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。说白了就是一大串字符串。本质上来说 JWT 也是 token。详情参考这篇文章:漫谈JWT 。值得一提的是JWT本身没有安全性可言,所以存储用户信息,尤其是敏感数据是一件很可怕的事情,建议不要存放这一类信息。详细生成JWT的代码我会贴到下面。

三、Redis

注:这里只对Redis 作简单介绍,说明Token与Redis的关系,以及为什么使用Redis存储Token
详细请参考:Redis【入门】就这一篇!
1.redis定义:redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。
2.Redis 在 Java Web 主要有两个应用场景:
存储 缓存 用的数据;
需要高速读/写的场合使用它快速读/写;
3.Token 为什么要存储到Redis?
本人也查阅了很多资料,总结下来无非三点:
1.Token 具有时效性,简单来说就是有效期,超过了这个期限Token 就会失效,需要用户重新登录。但是怎么在项目中实现这个时效性,这个时候就用到Redis,Redis天然支持支持设置过期时间以及通过一些第三方提供包的API到达自动续时效果。
2.Redis采用NoSQL技术,可以支持每秒十几万此的读/写操作,其性能远超数据库,在请求数量较多的时候,Redis也可以从容应对
3.用户登录信息一般不需要长效储存,放在内存中,可以减少数据库的压力

实现部分

引入依赖
        <!--Jedis是Redis官方推荐的Java连接开发工具。-->
		<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--         shiro 借用加密密码 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--        Jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.4.RELEASE</version>
        </dependency>

详细代码

1.用户注册
    @Override
    @Transactional
    public ReturnBody insertAdmin(SysAdmin admin) throws Exception {
       //SysAdmin 为管理员类
		admin.setAppName(Constants.PROJECT_NAME);//插入项目名
        String password = admin.getUserPassword();
        if(StringUtil.isEmpty(admin.getUserPassword())) {
            password ="123456";//默认密码为123456
        }
        PasswordHash passwordHash = PasswordUtil.encrypt(password);//采用shrio加密方式(密码加盐)PasswordUtil继承DigestUtil(Apache DigestUtils线程安全的类)
        admin.setUserPassword(passwordHash.getHexEncoded());//HexEncoded为加密过后的密文
        SysUser user = new SysUser(admin);
        ReturnBody result = userService.insertOrUpdateAuth(user);//判断用户邮箱手机号是否已经注册,用户名是否已经存在,这部分逻辑自己编写
		ResultUtil.throwException(result);//如果请求失败,则抛出异常,
        userMapper.insert(user);//插入用户表
        admin.setUserId(user.getUserId());//mybatis-plus 自动生成主键,获得主键
        this.save(admin);//插入管理员表
        userSaltMapper.insert(new SysUserSalt(admin.getUserId(), passwordHash.getSalt()));//插入盐值与用户关联表(每个用户都有一个特定的盐值增强安全性)
        Map<String, Object> map = new HashMap<String, Object>();
    	map.put("user_id", user.getUserId());
    	sysUserRoleMapper.deleteByMap(map);
        List<String> roleIds = admin.getRoleId();
        if (StringUtil.isNotEmpty(roleIds)){
            int a = userRoleMapper.insertdeRoles(admin.getUserId(),roleIds);
        }//插入用户角色关联信息
        /*
         * 根据admin的userId关联创建的附件列表
         */     
        ReturnBody body = new ReturnBody(admin);
        ((SysAdmin) body.getData()).setUserPassword(password);
        return body;
    }

PassHash类及PasswordUtil 加密方法

    PasswordUtil 加密方法
/**
     * 盐值加密
     * @param source
     * @return
     */
    public static PasswordHash encrypt(Object source) {
        return new PasswordHash(source);
    }
/**
 * 密码加密配置
 */
public class PasswordHash {
    /**
     * 加密方式
     */
	private String algorithmName;

	/**
	 * 加密次数
	 */
	private int hashIterations;

	/**
	 * 原密码
	 */
	private Object source;

	/**
	 * 盐值
	 */
	private String salt;

	/**
	 * 加密后的密文
	 */
	private String hexEncoded;

	/**
	 * 
	 * @param source
	 */
	public PasswordHash(Object source) {
	    this.algorithmName = "md5";//加密方式
	    this.hashIterations = 996;//加密次数
	    this.salt = UUIDUtil.getUUID();//盐值为随机数
	    this.source = source;//密码
	    this.hexEncoded = new SimpleHash(algorithmName, source, salt, hashIterations).toHex();//加密后的密文
	}

    @Override
    public String toString() {
        return "PasswordHash [algorithmName=" + algorithmName + ", hashIterations=" + hashIterations + ", source="
                + source + ", salt=" + salt + ", hexEncoded=" + hexEncoded + "]";
    }
	
}

2.用户登录
/**
     * 方法作用: 获取令牌(统一用户登录入口)
     * @param user
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "获取令牌(统一用户登录入口)" ,  notes="获取令牌(统一用户登录入口)")
    @PostMapping(value="/login")
    public String accessToken(@RequestBody Token token) throws Exception {
        String password = null;
        if (StringUtil.isNotEmpty(token.getPassword())){
             password = RSACoder.decrypt(token.getPassword());//解码 BASE64解码
            token.setPassword(password);
        }
         token = authService.selectByUserName(token);//通过用户名查到用户信息方便与传过来的token进行比对
        if (null==token) {
                ValueUtil.isError(ReturnCode.USERNAME_WETHER);
                //用户名不存在,抛出异常自行封装
			}else if(!PasswordUtil.checkPassword(password,token.getSalt(),token.getUserPassword())) {
			//password为用户输入的密码字符串,salt为用户插入之初设置的盐值,token.getUserPassword为数据库中通过密码加盐的方式形成的密文
			//帐号或密码错误,抛出异常自行封装
                ValueUtil.isError(ReturnCode.LOGIN_FAILED);
            }else if(StringUtil.isEmpty(token.getHomeUrl())) {
            //判断用户是否设置主页,方便用户登录成功后跳转登录界面
                ValueUtil.isError(ReturnCode.HOMEPAGE_NULL);
            } else {
               //设置用户权限(权限放面会陆续更新)
                token.setResSet(authService.selectResourceByUserId(token.getUserId()));
               //用户类型
                token.setSelectTypes(authService.selectTypes(token.getUserId()));
                token.setAppKey(appKey); //用户所属项目
                token.setIp(getUserIp());//用户登录id地址
                     String tokenCode = tokenManager.putToken(token);//创建token
                     setResponseTokenCode(tokenCode);
                     token = tokenManager.queryToken(tokenCode);//查看token 
                     token.setToken(tokenCode);

			}

        return ValueUtil.toJson(token);//返回token 
    }

##这里封装了一个Token管理器便于方便管理token

Token管理器
public class TokenManager {

	/**
	 * 
	 */
	@Autowired
	private RedisClientTokenUtil redisClient;//Redis操作Token封装类

	/**
	 * ios端设备标识前缀
	 */
	private static final String APPKEY_PREFIX_IOS = "IOS";

	/**
	 * android端设备标识前缀
	 */
	private static final String APPKEY_PREFIX_ANDROID = "ANDROID";

	/**
	 * moblie端设备标识前缀
	 */
	private static final String APPKEY_PREFIX_MOBILE = "MOBILE";

	/**
	 * web端设备标识前缀
	 */
	private static final String APPKEY_PREFIX_WEB = "WEB";

	/**
	 * ios终端token有效期
	 */
	private static final int EXPIRE_SECOND_IOS = 14 * 24 * 60 * 60;

	/**
	 * android终端token有效期
	 */
	private static final int EXPIRE_SECOND_ANDROID = 14 * 24 * 60 * 60;
	/**
	 * MOBILE终端token有效期
	 */
	private static final int EXPIRE_SECOND_MOBILE = 14 * 24 * 60 * 60;

	/**
	 * web终端token有效期
	 */
	private static final int EXPIRE_SECOND_WEB = 1 * 30 * 60;
	/**
	 * 管理员终端token有效期
	 */
	private static final int ADMINISTRATOR_WEB = 3 * 60 * 60;

	/**
	 * 其他终端token有效期
	 */
	private static final int EXPIRE_SECOND_OTHER = EXPIRE_SECOND_WEB;


	/**
	 * 获取token
	 * 
	 * @param token
	 * @return
	 * @throws Exception
	 */
	public String putToken(Token token) throws Exception {
		String tokenCode = null;
		if (token != null) {
			token.setExpireSecond(createExpireSecond(token));//延长token 有效期
			tokenCode = createTokenCode(token);//创建token
			String key = getKey(tokenCode);//项目名+tokenCode
			redisClient.set(key, token, token.getExpireSecond());
		}
		return tokenCode;
	}

	/**
	 * 校验token
	 * 
	 * @param tokenCode
	 * @return
	 * @throws Exception
	 */
	public boolean checkToken(String tokenCode) throws Exception {
		if (!StringUtils.isEmpty(tokenCode)) {
			String key = getKey(tokenCode);
			if (redisClient.exists(key)) {
				Token token = redisClient.get(key);
				// 重置有效期
				redisClient.setExpire(key, token.getExpireSecond());
				return true;
			}
		}
		return false;
	}
	

	/**
	 * 查看token
	 * 
	 * @param tokenCode
	 * @return Token
	 * @throws Exception
	 */
	public Token queryToken(String tokenCode) throws Exception {
		String key = getKey(tokenCode);
		Token token = redisClient.get(key);
		if (token != null) {
			redisClient.setExpire(key, token.getExpireSecond());

		}
		if (StringUtil.isEmpty(token)){
			ValueUtil.isError(ReturnCode.UNAUTHORIZED);
		}

		return token;
	}

	/**
	 * 初始化token有效期
	 * 
	 * @param token
	 * @return
	 */
	private int createExpireSecond(Token token) {
		int expireSecond = EXPIRE_SECOND_OTHER;
		if (token != null) {

			String appKey = token.getAppKey();
			if (!StringUtils.isEmpty(appKey)) {
				String prefix = appKey;
				switch (prefix) {
				case APPKEY_PREFIX_ANDROID:
					expireSecond = EXPIRE_SECOND_ANDROID;
					break;
				case APPKEY_PREFIX_IOS:
					expireSecond = EXPIRE_SECOND_IOS;
					break;
				case APPKEY_PREFIX_MOBILE:
					expireSecond = EXPIRE_SECOND_MOBILE;
					break;
				case APPKEY_PREFIX_WEB:
					expireSecond = EXPIRE_SECOND_WEB;
				}
			} else {
				if (token.getUserType() == 1) {
					expireSecond = ADMINISTRATOR_WEB;
				}
			}

		}

		return expireSecond;
	}

	/**
	 * 根据tokenCode获取key
	 * 
	 * @param tokenCode
	 * @return
	 */
	private String getKey(String tokenCode) {
		return Constants.TOKEN_PREFIX + tokenCode;
	}

	/**
	 * 生成tokenCode
	 * 
	 * @param token
	 * @return
	 */
	private String createTokenCode(Token token) {
		String tokenCode = null;
		if (StringUtil.isNotEmptyObjects(token.getUserId(),token.getUserName())) {
				tokenCode=UserUtils.createToken(token.getUserId(),token.getUserName(),token.getExpireSecond());
		}
		return tokenCode;
	}

//createToken 方法直接写在这里
    public static String createToken(String userId, String userName, int expireSecond) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成签名密钥
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(RSAUtil.privateKey);//RSAUtil.privateKey为私钥

        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        //添加构成JWT的参数
        JwtBuilder builder = Jwts.builder()
                .claim("userId", userId) // 设置载荷信息
                .claim("userName",userName)
                .signWith(signatureAlgorithm, signingKey);

        //生成JWT
        return builder.compact();

    }
}
密码校验
   /**
     * 密码校验
     * @param source
     * @param salt
     * @param password
     * @return
     */
    public static boolean checkPassword(Object source, String salt, String password) {
    //判断参数非空 ,自行封装  此处省略
        return password.equals(encryptString(source, salt));//再次加密 比对加密结果

    }
  /**
     * 盐值加密
     * @param source
     * @return
     */
    public static String encryptString(Object source, String salt) {
        return new PasswordHash(source, salt).getHexEncoded();
    }

注:本文章为本人学习记录总结


  • 10
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
单点登录(Single Sign-On,简称SSO)是一种身份验证技术,可以让用户只需一次登录,就可以访问多个应用程序。在实际开发中,我们可以使用Spring BootJWTRedis来实现单点登录功能。 下面是实现单点登录的步骤: 1. 创建Spring Boot项目并引入所需依赖:spring-boot-starter-web、spring-boot-starter-data-redis和jjwt。 2. 创建一个User实体类,包含用户名和密码等信息。 3. 创建一个UserService,实现对用户信息的操作,包括注册、登录等。 4. 引入JWT依赖后,我们需要创建一个JWTUtil类,实现token的生成和解析。 5. 创建一个LoginController,用于处理用户的登录请求。在登录成功后,生成token并将其存储到Redis中。 6. 创建一个AuthController,用于验证用户的token是否有效。在验证成功后,可以获取用户信息并返回。 7. 在需要进行单点登录验证的应用程序中,只需要在请求中携带token,并调用AuthController进行验证即可。 具体实现细节可以参考以下代码示例: User实体类: ```java public class User { private String username; private String password; // 省略setter和getter方法 } ``` UserService接口: ```java public interface UserService { void register(User user); String login(String username, String password); } ``` UserService实现类: ```java @Service public class UserServiceImpl implements UserService { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public void register(User user) { // 省略用户注册逻辑 } @Override public String login(String username, String password) { // 省略用户登录逻辑 // 登录成功后生成token并存储到Redis中 String token = JWTUtil.generateToken(username); redisTemplate.opsForValue().set(username, token, 30, TimeUnit.MINUTES); return token; } } ``` JWTUtil类: ```java public class JWTUtil { private static final String SECRET_KEY = "my_secret_key"; private static final long EXPIRATION_TIME = 3600000; public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } } ``` LoginController: ```java @RestController public class LoginController { @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<String> login(@RequestBody User user) { String token = userService.login(user.getUsername(), user.getPassword()); return ResponseEntity.ok(token); } } ``` AuthController: ```java @RestController public class AuthController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/auth") public ResponseEntity<User> auth(@RequestHeader("Authorization") String token) { String username = JWTUtil.getUsernameFromToken(token); if (StringUtils.isEmpty(username)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String redisToken = redisTemplate.opsForValue().get(username); if (!token.equals(redisToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } User user = new User(); user.setUsername(username); return ResponseEntity.ok(user); } } ``` 在请求中携带token的示例: ```java @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> { String token = // 从Redis中获取token request.getHeaders().add("Authorization", token); return execution.execute(request, body); })); return restTemplate; } } ``` 以上就是使用Spring BootJWTRedis实现单点登录的步骤和示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值