飞天班第31节:前台登录注册业务实现

2020/05/06

1、登录业务

SSO(Single Sign on)模式

淘宝天猫单点登录原理:

这个sso模型与游乐场的门票是一个道理的,凭证token可以访问任意服务,不能泄漏,更安全的解决方案:

1、缓存定时失效

2、两次登录信息不一致(电脑,手机),进行短信验证码验证,如支付宝

2、集成JWT

登录后,传统的身份验证,可以在拦截器或过滤器进行身份验证,但是这样网站服务与身份验证在同一个工程中,耦合性强,单体应用可以使用,但是多个微服务下就不行了,因为无法横向扩展,解决方案:

  • session共享,服务太多会存在共享延迟的问题,可以把session缓存到redis中。
  • cookie令牌,用户信息缓存到第三方,代表解决方案 JWT(JSON Web Token)

JWT令牌

  1. 透明令牌

    随机生成,没有办法猜测这个是如何存储和颁发!所有的请求必须要要到 OAuth2授权服务的的检测,才能判断这个令牌是否有效!

  2. 自包含令牌

    授权服务器颁发的,包含用户的数据,声明式检查前面,颁发者、期望的接收人,,,,, 可以在本地来进行校验!

JWT组成

JWT对象是一个很长的字符串,通过.来进行分割为三个字符串!

1、Header就是JWT 元数据的JSON对象

{
    "alg": "HS256",
    "type": "JWT"
    .....
}

2、Claims就是有效载荷(具体的主体信息)

7个默认的字段:
发行人
到期时间
主题
用户
在此之前不可用
发布时间
jwt id
我们还可以添加自己的信息
{
    "name": "coding",
    ///...
}

3、Signature就是签名哈希

签名哈希 就是对上面两部分数据的签名,通过指定的算法,生成哈希,确保数据不会被篡改!
首先,需要指定一个密码(secret),保存到服务器中,secret不能向用户公开。
根据某种算法,生成一个新的密码。

JWT的好处

  • 不仅可以用于认证,还可以用来交换信息。可以减少请求服务器的次数!

  • (自包含令牌)生成的token一般会包含用户的信息,id/昵称、头像…..,
  • 存储在客户端的,不会占用服务器端的内存资源!

JWT的缺点

  • JWT 默认不加密的,你也可以加密,生成原始的令牌后然后对其再加密!同理,服务器拿到token后先解密得到明文的token字符串,再解析与校验token,所以没有被加密的时候,不建议存放私密数据!

JWT头与calims载荷默认不加密的,通过nimbus-jose-jwt这个开源库解析出payload载荷信息,包括jti、exp到期时间、主题、自己添加的信息等,无需通过签名校验token,就可以获取这些payload信息,为了避免token被篡改,一般都是先用Signature(签名)校验token再解析token获取payload。

nimbus-jose-jwt 使用他可以生成或者解析对称加密或者非对称加密的的JWT. JWS是JWT规范的落地实现。

集成到项目中

用户中心模块 edu-ucenter

1、pom.xml导入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

2、创建工具类JwtUtils

public class JwtUtils {
	// 这个只有在服务端有
	public static final String APP_SECRET = "[asdhakdjsakjdlasjdu13123123_123!@";
	public static final long EXPIRE = 1000*60*60*24;

	public static String getJwtToken(String id,String nickname){
		String jwtToken = Jwts.builder()
				.setHeaderParam("typ", "JWT")
				.setHeaderParam("alg", "HS256")
				.setSubject("member-user")  // 这个token字符串的标题,也可以使用id唯一值标识
				.setIssuedAt(new Date())    // 创建时间
				.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
				.claim("id", id)   // 自包含的数据
				.claim("nickname", nickname) // 自包含的数据
				.signWith(SignatureAlgorithm.HS256, APP_SECRET) // 签名加密
				.compact();
		// 就是一个指定规则的字符串jwtToken,这里保存用户的信息
		return jwtToken;
	}

	// 检测token
	public static boolean checkToken(String jwtToken){
		if (StringUtils.isEmpty(jwtToken)) return false;
		try {
			Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	// 根据用户请求头中的token获取用户的id
	public static String getMemberIdByJwtToken(HttpServletRequest request) {
		String token = request.getHeader("token");
		if (StringUtils.isEmpty(token)) return "";
		Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
		Claims body = claimsJws.getBody();
		return (String) body.get("id");
	}
}

前台项目就可以使用jwt,给用户生成一个加密的token,服务端进行校验解析。

3、阿里云短信验证码

开通服务

阿里云业务模型:

这里使用跟oss\vod同一个授权子账号,

登录阿里云控制台

短信业务开通

点击进去,开通

给用户组授权短信业务AliyunDysmsFullAccess

计费规则

短信规则

超过70个字数,按两条短信计算,

计费规则

https://www.aliyun.com/price/product?spm=5176.8911205.0.0.1d541cbepQsxQP#/sms/detail

上限规则

签名与短信模版申请

个人只配拥有一个签名,模版和签名申请一定要正规,否则通过不了。

4、短信微服务

短信模版申请通过后,我们在项目中集成短信服务,找到短信服务的帮助文档,添加依赖

使用示例:

新建module maven工程edu-sms 短信微服务

发送短信验证码,测试

public class TestSMS {
    @Test
    public void test(){
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "<accessKeyId>", "<accessSecret>");
        // 连接到客户端
        IAcsClient client = new DefaultAcsClient(profile);
        // 短信请求
        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");// 不能改动
        request.setSysVersion("2017-05-25"); // 版本时间,不能改动
        // api事件
        request.setSysAction("SendSms");

        // 具体的发送
        request.putQueryParameter("PhoneNumbers", "187xxxx39");
        request.putQueryParameter("SignName", "<签名>");
        request.putQueryParameter("TemplateCode", "<模版CODE>");
        // 验证码
        HashMap<String, Object> param = new HashMap<>();
        param.put("code",2333);
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));

        try {
            // 请求发送获得响应
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }
}

测试没问题,写接口服务

web层接口的定义SmsApiController

@GetMapping("/send/{phone}")
public R code(@PathVariable("phone") String phone){
  // 先判断否已经发送过了,从redis获取key
  String code = redisTemplate.opsForValue().get(phone);
  if (!StringUtils.isEmpty(code)){
    return R.ok();
  }

  // 生成验证码
  code = UUID.randomUUID().toString().substring(0, 4);
  HashMap<String, Object> param = new HashMap<>();
  param.put("code",code);

  boolean isSend = smsService.send(phone, "SMS_189520818", param); // SMS_189520818 就是短信模版CODE
  if (isSend){
    // 缓存到redis中,60秒失效
    redisTemplate.opsForValue().set(phone,code,60,TimeUnit.SECONDS);
    return R.ok();
  }else {
    return R.error().message("短信发送失败");
  }
}

业务层实现类SmsServiceImpl.java

@Service
public class SmsServiceImpl implements SmsService {
    @Override
    public boolean send(String phoneNums, String tempalteCode, Map<String, Object> param) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", ConstantPropertiesUtils.ACCESS_KEY_ID, ConstantPropertiesUtils.ACCESS_KEY_SECRET);
        // 连接到客户端
        IAcsClient client = new DefaultAcsClient(profile);
        // 短信请求
        CommonRequest request = new CommonRequest();
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");// 不能改动
        request.setVersion("2017-05-25"); // 版本时间,不能改动
        // api事件
        request.setAction("SendSms");

        // 具体的发送
        request.putQueryParameter("PhoneNumbers", phoneNums);
        request.putQueryParameter("SignName", "狂神说Java学习网站");
        request.putQueryParameter("TemplateCode", tempalteCode);
        // 验证码
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));

        try {
            // 请求发送获得响应
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return response.getHttpResponse().isSuccess();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
        return false;
    }
}

5、理解OAuth2协议

什么是oauth2

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息。 OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0

oauth2的清楚认识,查看《OAuth2最简向导》

oauth2的应用

主要应用于现代微服务安全

  • 传统的单体应用安全

​ 应用服务器保存了授权过的用户session信息

  • 现代微服务安全

    核心的技术不是用户名和密码,而是token,由AuthServer颁发token,用户使用token进行登录

典型应用

oauth2解决的问题

6、整合微信登录

第三方应用使用微信登录的流程如下:

主要理解为两步:

第一步:请求CODE(生成授权URL)

第二步:通过code获取access_token(开发回调URL)

Post Directory