什么是单点登录?
定义:
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
简单来说就是多个服务器的“REMEMBER ME”功能。
扩展阅读:什么是单点登录(SSO)
使用技术
JWT
-
JWT(Json web token ):由于HTTP是无状态的协议,这意味着服务器无法确认用户的信息,因此我们则需要JWT来代替Session,告知服务器并亮明身份。
Shiro
- Shiro:是一个轻量级、灵活度高的鉴权框架,系统之前已做好shiro作为登录和权限控制,通过自定义实现realm实现用户名、密码验证登录
- 认证就是要核验用户的身份,比如说通过用户名和密码来检验用户的身份。说简单一些,认证就是登陆。登陆之后Shiro要记录用户成功登陆的凭证。
- 授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登陆之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。
- Shiro可以利用
HttpSession
或者Redis
存储用户的登陆凭证,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的HttpSession
或者Redis
中的认证与授权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。
Redis
- Redis:储存用户的登录凭证,也就是Token,并设计一定的有效期,可实现在一定时间内的单点登录。
实现步骤:
用户在Emos登陆页面点击登陆按钮,然后小程序把临时授权字符串
提交给后端Java系统。后端Java系统拿着临时授权字符串换取到openid
,我们查询用户表中是否存在这个openid
。如果存在,意味着该用户是已注册用户,可以登录。如果不存在,说明该用户尚未注册,目前还不是我们的员工,所以禁止登录。
总体流程如上图所示:
- 通过小程序端获取临时授权码,
- 传到后端获取到与用户唯一对应的openId,
- 根据openId获取对应用户信息实现登录
- 通过Shiro鉴权获取
- 通过JWT获取token
- 存入redis
获取openId登录:
参考官方文档时序图:
-
前端:
-
-
在
uni-app
框架中,包含了原生微信小程序的wx
对象,我们可以像写原生微信小程序代码一样,通过wx
对象调用各种方法。使用uni
跨平台的对象,我们调用uni对象中的方法,HBuilderX在编译代码的时候,会把uni
对象翻译成目标平台的对象,比如微信平台就是wx
对象,支付宝平台就是my
对象,所以uni
对象的跨平台性更好 -
返回参数:
参数名 说明 authResult 登录服务商提供的登录信息,服务商不同返回的结果不完全相同 code 小程序专有,用户登录凭证。开发者需要在开发者服务器后台,使用 code 换取 openid 和 session_key 等信息 errMsg 描述信息
-
-
实例代码:
login: function() { let that = this; uni.login({ provider: 'weixin', success: function(resp) { let code = resp.code; let token = uni.getStorageSync('token'); that.ajax(that.url.login, 'POST', { code: code }, function(resp) { let permission = resp.data.permission; uni.setStorageSync('permission', permission); //跳转到登陆页面 uni.switchTab({ url: '../index/index' }); }); }, fail: function(e) { uni.showToast({ icon: 'none', title: '执行异常' }); } }); }
-
-
后端:
-
获取微信小程序openId方法
private String getOpenId(String code) { String url = "https://api.weixin.qq.com/sns/jscode2session"; HashMap map = new HashMap<String, Object>(); map.put("appid", appId); map.put("secret", appSecret); //刚刚获取的临时授权字符串 map.put("js_code", code); map.put("grant_type", "authorization_code"); String response = HttpUtil.post(url, map); JSONObject json = JSONUtil.parseObj(response); String openId = json.getStr("openid"); if (StrUtil.isEmpty(openId)) { throw new RuntimeException("临时登陆凭证异常"); } return openId; }
-
通过具体获取的openId去找对应的用户
-
获取消息,并跳转至首页
-
创建Token
-
导入依赖:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
-
根据userId 创建token
@Component @Slf4j public class JwtUtil { //密钥 @Value("${emos.jwt.secret}") private String secret; //过期时间 @Value("${emos.jwt.expire}") private int expire; public String createToken(int userId) { Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire).toJdkDate(); Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象 JWTCreator.Builder builder = JWT.create(); String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm); return token; } public int getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userId").asInt(); } catch (Exception e) { throw new EmosException("令牌无效"); } } public void verifierToken(String token) { Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象 JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); } }
实现登录逻辑
-
Service层:
@Slf4j @Service @Scope("prototype") //多例防止多个用户同时登录 public class UserServiceImpl implements UserService { //... @Override public Integer login(String code) { // 根据获取的授权码获取微信账号的userid String openId = getOpenId(code); //根据openId找对应的用户信息 Integer id = userDao.searchIdByOpenId(openId); if (id == null) { throw new EmosException("账户不存在"); } //从消息队列中接收消息,转移到消息表 messageTask.receiveAysnc(id+""); return id; } //... }
-
Web层:
@Data @ApiModel public class LoginForm { //使用validation框架做校验 @NotBlank(message = "临时授权不能为空") private String code; }
@RestController @RequestMapping("/user") @Api("用户模块web接口") public class UserController { @PostMapping("/login") @ApiOperation("登陆系统") public R login(@Valid @RequestBody LoginForm form, @RequestHeader("token") String token) { Integer id; if (StrUtil.isNotEmpty(token)) { try { //验证令牌的有效性 jwtUtil.verifierToken(token); } catch (TokenExpiredException e) { //如果令牌过期就生成新的令牌 id = userService.login(form.getCode()); token = jwtUtil.createToken(id); saveCacheToken(token, id); } id = jwtUtil.getUserId(token); } else { id = userService.login(form.getCode()); token = jwtUtil.createToken(id); saveCacheToken(token, id); } Set<String> permsSet = userService.searchUserPermissions(id); return R.ok("登陆成功").put("token", token).put("permission", permsSet); } //将token保存至redis //下一步详解 private void saveCacheToken(String token, int userId) { redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS); } }
RedisTemplate
SpringBoot集成redis操作类:
-
maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
使用方法:
//直接注入即可 @Autowired private RedisTemplate<String, String> redisTemplate;
-
常用api:
不同操作类型对象:
分别对应geo、hash、list、set、String、zset等Redis数据类型
-
常用String,即
boundValueOps
ValueOperations<String, String> ops = redisTemplate.opsForValue(); //void set(K key, V value, long timeout, TimeUnit unit); ops.set(token, userId + "", cacheExpire, TimeUnit.DAYS); //void set(K key, V value); //直接保存键值对 //根据key直接删除value Boolean result = redisTemplate.delete(token); //顺序递增 redisTemplate.boundValueOps(token).increment(3L); //顺序递减 redisTemplate.boundValueOps(token).increment(-3L);
-