单点登录课程笔记

资料

hi,这是我用百度网盘分享的文件~复制这段内容打开「百度网盘」APP即可获取。
链接:https://pan.baidu.com/s/17v_azZD6tHJ2Y5WJJw4tow?pwd=6666
提取码:6666 --来自百度网盘超级会员V4的分享

文章目录

  • 单点登录
    • session跨域
      • 代码实现
    • nginx session共享
    • Token机制
      • 传统的session身份认证
      • Token 身份认证
    • JSON WebToken (JWT) 机制
      • 1JWT数据结构
        • 1.1header
        • 1.2payload
        • 1.3Signature
      • 2.JWT执行流程
    • 基于JWT机制的单点登录
      • 实现
      • 注意
      • token保存位置
      • webstorage
    • Restful接口设计
      • Restful简介
      • Restful简述
      • Restful特性
        • 1.普通架构
        • 2.Restful架构
        • 3.Restful操作方式
        • 案例代码
        • 响应状态编码
    • 接口安全机制
      • 1安全机制的设计方案
        • 1.1单向加密
        • 1.2双向加密
          • 1.2.1对称加密
          • 1.2.2非对称加密
      • 2DES加密
      • 3AES加密
      • 4使用场景
      • 5对比

单点登录

多系统,单一位置登录,实现多系统同时登录的一种解决方案
单点登录一般用于互相授信的系统,实现单一位置登录,全系统有效的

三方登录,某系统,使用其他系统的用户,实现本系统登录的方式。解决信息孤岛和用户不对等的实现方案 OAuth2.0

session跨域

所谓Session跨域就是捐弃了系统提供的 Session,而使用自定义类似 Session 的机制来保存客户端数据的一种解决方案。
如:通过设置cookie的domain来实现 cookie 的跨域传递。 在cookie中传递一个自定义的 session_id。这个session_id 是客户端的唯一标记。将这个标记作为 key,将客户端需要保存的数据作为 value 在服务端进行保存(数据库保存或NoSQL保存)。这种机制就是Session的跨域解决。

jsonp 只是跨域访问

什么是跨域,客户端请求的时候,请求的服务器,不是同一个IP,端口,域名,主机名的时候,都称为跨域

对于集群来说是跨应用,可以用其他技术实现

什么是域:在应用模型,一个完整的,有独立访问路径的功能集合称为一个与。如,百度称为一个应用或系统。百度下有若干个域。域信息有时也称为多级域名。域的划分,以ip,端口,域名,主机名为标准,实现划分。

代码实现

单点登录课程笔记_第1张图片
见资料文件

修改hosts文件

# 虚拟本地域名
192.168.159.131		www.test.com
192.168.159.131		sso.test.com

JSESSIONID: 系统http的唯一标记

nginx session共享

nginx中的ip_hash技术能够将某个ip的请求定向到同一台后端,这样一来这个ip下的某个客户端和某个后端就能建立起稳固的session,ip_hash是在upstream配置中定义的,具体如下:

upstream nginx.example.com
{
    server 127.0.0.1:8080 weight 1;
    server 127.0.0.1:808 weight 2;
    ip_hash;
}
server
{
    listen 80;
    location /
    {
        proxy_pass
        http://nginx.example.com;
        proxy_set_header Host  $http_host;
        proxy_set_header Cookie $http_cookie;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size  100m;
    }
}

ip_hash是容易理解的,但是因为仅仅能用ip这个因子来分配后端,因此ip_hash是有缺陷的,不能在一些情况下使用:
nginx不是最前端的服务器。
ip_hash要求nginx一定是最前端的服务器,否则nginx得不到正确ip,就不能根据ip作hash。譬如使用的是squid为最前端,那么nginx取ip时只能得到squid的服务器ip地址,用这个地址来作分流是肯定错乱的。
nginx的后端还有其它方式的负载均衡。
假如nginx后端又有其它负载均衡,将请求又通过另外的方式分流了,那么某个客户端的请求肯定不能定位到同一台session应用服务器上。

Token机制

传统的session身份认证

  HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。

  解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。

  上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。

这种认证中出现的问题是:
  Session:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
  可扩展性:在服务端的内存中使用Session存储登录信息,伴随而来的是可扩展性问题。
  CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
  CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展性是最突出的。因此我们有必要去寻求一种更有行之有效的方法。

Token 身份认证

  使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
  客户端使用用户名、密码请求登录
  服务端收到请求,去验证用户名、密码
  验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 、Session Storage里
  客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

使用Token验证的优势:
  无状态、可扩展
  在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
  安全性
  请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。

JSON WebToken (JWT) 机制

  JWT是一种紧凑自包含的,用于在多方传递JSON对象的技术。传递的数据可以使用数字签名增加其安全行。可以使用HMAC加密算法(对称加密)或RSA公钥/私钥加密(非对称加密)方式。
  紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。
  自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能。
  JWT一般用于处理用户身份验证数据信息交换
  用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据签名来保证数据的有效性和安全性。
官网: jwt.io

1JWT数据结构

JWT的数据结构是 : A.B.C。 由字符点‘.’来分隔三部分数据。
A - header 头信息
B - payload (有效荷载?,非隐私数据)
C - Signature 签名(header + payload + 幂时 加密后的密文)

1.1header

数据结构: {“alg”: “加密算法名称”, “typ” : “JWT”}
alg是加密算法定义内容,如:HMAC SHA256 或 RSA
typ是token类型,这里固定为JWT。

1.2payload

  在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。
  已注册信息:值jwt中已经有的标准注册信息,payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。
  公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。可以检查有效性
  公开数据和私有数据可以由程序员任意定义。

注意:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。

1.3Signature

  签名信息。这是一个由开发者提供的信息。是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据的之前。先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head.加密后的payload。再使用相同的加密算法,对加密后的数据和签名信息进行加密。得到最终结果。

2.JWT执行流程

单点登录课程笔记_第2张图片

基于JWT机制的单点登录

实现

单点登录课程笔记_第3张图片

<dependencies>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-contextartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-webmvcartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-aspectsartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		
		<dependency>
			<groupId>com.auth0groupId>
			<artifactId>java-jwtartifactId>
			<version>3.3.0version>
		dependency>
		
		<dependency>
		    <groupId>io.jsonwebtokengroupId>
		    <artifactId>jjwtartifactId>
		    <version>0.9.0version>
		dependency>
		
		<dependency>
		    <groupId>com.fasterxml.jackson.coregroupId>
		    <artifactId>jackson-databindartifactId>
		    <version>2.9.5version>
		dependency>
		<dependency>
			<groupId>javax.servletgroupId>
			<artifactId>jstlartifactId>
			<version>1.2version>
		dependency>
		<dependency>
			<groupId>javax.servletgroupId>
			<artifactId>servlet-apiartifactId>
			<version>2.5version>
			<scope>providedscope>
		dependency>
		<dependency>
			<groupId>javax.servlet.jspgroupId>
			<artifactId>jsp-apiartifactId>
			<version>2.2version>
			<scope>providedscope>
		dependency>
	dependencies>

JWTResponseData

package com.sxt.sso.commons;

/**
 * 发送给客户端的数据对象。
 * 商业开发中,一般除特殊请求外,大多数的响应数据都是一个统一类型的数据。
 * 统一数据有统一的处理方式。便于开发和维护。
 */
public class JWTResponseData {

	private Integer code;// 返回码,类似HTTP响应码。如:200成功,500服务器错误,404资源不存在等。
	
	private Object data;// 业务数据
	
	private String msg;// 返回描述
	
	private String token;// 身份标识, JWT生成的令牌。

	public Integer getCode() {
		return code;
	}

	public void setCode(Integer code) {
		this.code = code;
	}

	public Object getData() {
		return data;
	}

	public void setData(Object data) {
		this.data = data;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}

	public String getToken() {
		return token;
	}

	public void setToken(String token) {
		this.token = token;
	}
	
}

JWTResult

package com.sxt.sso.commons;

import io.jsonwebtoken.Claims;

/**
 * 结果对象。
 */
public class JWTResult {

	/**
	 * 错误编码。在JWTUtils中定义的常量。
	 * 200为正确
	 */
	private int errCode;

	/**
	 * 是否成功,代表结果的状态。
	 */
	private boolean success;

	/**
	 * 验证过程中payload中的数据。
	 */
	private Claims claims;

	public int getErrCode() {
		return errCode;
	}

	public void setErrCode(int errCode) {
		this.errCode = errCode;
	}

	public boolean isSuccess() {
		return success;
	}

	public void setSuccess(boolean success) {
		this.success = success;
	}

	public Claims getClaims() {
		return claims;
	}

	public void setClaims(Claims claims) {
		this.claims = claims;
	}
	
}

JWTSubject

package com.sxt.sso.commons;

/**
 * 作为Subject数据使用。也就是payload中保存的public claims
 * 其中不包含任何敏感数据
 * 开发中建议使用实体类型。或BO,DTO数据对象。
 */
public class JWTSubject {

	private String username;

	public JWTSubject() {
		super();
	}

	public JWTSubject(String username) {
		super();
		this.username = username;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}
	
}

JWTUsers

package com.sxt.sso.commons;

import java.util.HashMap;
import java.util.Map;

/**
 * 用于模拟用户数据的。开发中应访问数据库验证用户。
 */
public class JWTUsers {

	private static final Map<String, String> USERS = new HashMap<>(16);
	
	static{
		for(int i = 0; i < 10; i++){
			USERS.put("admin"+i, "password"+1);
		}
	}
	
	// 是否可登录
	public static boolean isLogin(String username, String password){
		if(null == username || username.trim().length() == 0){
			return false;
		}
		String obj = USERS.get(username);
		if(null == obj || !obj.equals(password)){
			return false;
		}
		
		return true;
	}
	
}

JWTUtils

package com.sxt.sso.commons;

import java.util.Date;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;

/**
 * JWT工具
 */
public class JWTUtils {
	
	// 服务器的key。用于做加解密的key数据。 如果可以使用客户端生成的key。当前定义的常亮可以不使用。
	private static final String JWT_SECERT = "test_jwt_secert" ;
	private static final ObjectMapper MAPPER = new ObjectMapper();
	public static final int JWT_ERRCODE_EXPIRE = 1005;//Token过期
	public static final int JWT_ERRCODE_FAIL = 1006;//验证不通过

	public static SecretKey generalKey() {
		try {
			// byte[] encodedKey = Base64.decode(JWT_SECERT); 
			// 不管哪种方式最终得到一个byte[]类型的key就行
			byte[] encodedKey = JWT_SECERT.getBytes("UTF-8");
		    SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
		    return key;
		} catch (Exception e) {
			e.printStackTrace();
			 return null;
		}
	}
	/**
	 * 签发JWT,创建token的方法。
	 * @param id  jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
	 * @param iss jwt签发者
	 * @param subject jwt所面向的用户。payload中记录的public claims。当前环境中就是用户的登录名。
	 * @param ttlMillis 有效期,单位毫秒
	 * @return token, token是一次性的。是为一个用户的有效登录周期准备的一个token。用户退出或超时,token失效。
	 * @throws Exception
	 */
	public static String createJWT(String id,String iss, String subject, long ttlMillis) {
		// 加密算法
		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
		// 当前时间。
		long nowMillis = System.currentTimeMillis();
		// 当前时间的日期对象。
		Date now = new Date(nowMillis);
		SecretKey secretKey = generalKey();
		// 创建JWT的构建器。 就是使用指定的信息和加密算法,生成Token的工具。
		JwtBuilder builder = Jwts.builder()
				.setId(id)  // 设置身份标志。就是一个客户端的唯一标记。 如:可以使用用户的主键,客户端的IP,服务器生成的随机数据。
				.setIssuer(iss)
				.setSubject(subject)
				.setIssuedAt(now) // token生成的时间。
				.signWith(signatureAlgorithm, secretKey); // 设定密匙和算法
		if (ttlMillis >= 0) { 
			long expMillis = nowMillis + ttlMillis;
			Date expDate = new Date(expMillis); // token的失效时间。
			builder.setExpiration(expDate);
		}
		return builder.compact(); // 生成token
	}
	
	/**
	 * 验证JWT
	 * @param jwtStr
	 * @return
	 */
	public static JWTResult validateJWT(String jwtStr) {
		JWTResult checkResult = new JWTResult();
		Claims claims = null;
		try {
			claims = parseJWT(jwtStr);
			checkResult.setSuccess(true);
			checkResult.setClaims(claims);
		} catch (ExpiredJwtException e) { // token超时
			checkResult.setErrCode(JWT_ERRCODE_EXPIRE);
			checkResult.setSuccess(false);
		} catch (SignatureException e) { // 校验失败
			checkResult.setErrCode(JWT_ERRCODE_FAIL);
			checkResult.setSuccess(false);
		} catch (Exception e) {
			checkResult.setErrCode(JWT_ERRCODE_FAIL);
			checkResult.setSuccess(false);
		}
		return checkResult;
	}
	
	/**
	 * 
	 * 解析JWT字符串
	 * @param jwt 就是服务器为客户端生成的签名数据,就是token。
	 * @return
	 * @throws Exception
	 */
	public static Claims parseJWT(String jwt) throws Exception {
		SecretKey secretKey = generalKey();
		return Jwts.parser()
			.setSigningKey(secretKey)
			.parseClaimsJws(jwt)
			.getBody(); // getBody获取的就是token中记录的payload数据。就是payload中保存的所有的claims。
	}
	
	/**
	 * 生成subject信息
	 * @param subObj - 要转换的对象。
	 * @return java对象->JSON字符串出错时返回null
	 */
	public static String generalSubject(Object subObj){
		try {
			return MAPPER.writeValueAsString(subObj);
		} catch (JsonProcessingException e) {
			e.printStackTrace();
			return null;
		}
	}
	
}

JWTController

package com.sxt.sso.controller;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sxt.sso.commons.JWTResponseData;
import com.sxt.sso.commons.JWTResult;
import com.sxt.sso.commons.JWTSubject;
import com.sxt.sso.commons.JWTUsers;
import com.sxt.sso.commons.JWTUtils;

@Controller
public class JWTController {

	@RequestMapping("/testAll")
	@ResponseBody
	public Object testAll(HttpServletRequest request){
		
		String token = request.getHeader("Authorization");
		JWTResult result = JWTUtils.validateJWT(token);
		
		JWTResponseData responseData = new JWTResponseData();
		
		if(result.isSuccess()){
			responseData.setCode(200);
			responseData.setData(result.getClaims().getSubject());
			// 重新生成token,就是为了重置token的有效期。
			String newToken = JWTUtils.createJWT(result.getClaims().getId(), 
					result.getClaims().getIssuer(), result.getClaims().getSubject(), 
					1*60*1000);
			responseData.setToken(newToken);
			return responseData;
		}else{
			responseData.setCode(500);
			responseData.setMsg("用户未登录");
			return responseData;
		}
		
	}
	
	@RequestMapping("/login")
	@ResponseBody
	public Object login(String username, String password){
		JWTResponseData responseData = null;
		// 认证用户信息。本案例中访问静态数据。
		if(JWTUsers.isLogin(username, password)){
			JWTSubject subject = new JWTSubject(username);
			String jwtToken = JWTUtils.createJWT(UUID.randomUUID().toString(), "sxt-test-jwt", 
					JWTUtils.generalSubject(subject), 1*60*1000);
			responseData = new JWTResponseData();
			responseData.setCode(200);
			responseData.setData(null);
			responseData.setMsg("登录成功");
			responseData.setToken(jwtToken);
		}else{
			responseData = new JWTResponseData();
			responseData.setCode(500);
			responseData.setData(null);
			responseData.setMsg("登录失败");
			responseData.setToken(null);
		}
		
		return responseData;
	}
	
}

applicationContext-mvc.xml


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="com.sxt.sso.controller" />
	
	<mvc:annotation-driven />
	
	<mvc:resources location="/js/" mapping="/js/**">mvc:resources>

beans>

web.xml


<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>sso-cross-domaindisplay-name>
  <welcome-file-list>
    <welcome-file>index.htmlwelcome-file>
    <welcome-file>index.htmwelcome-file>
    <welcome-file>index.jspwelcome-file>
    <welcome-file>default.htmlwelcome-file>
    <welcome-file>default.htmwelcome-file>
    <welcome-file>default.jspwelcome-file>
  welcome-file-list>
  <filter>
    <filter-name>charSetFilterfilter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
    <init-param>
      <param-name>encodingparam-name>
      <param-value>UTF-8param-value>
    init-param>
  filter>
  <filter-mapping>
    <filter-name>charSetFilterfilter-name>
    <url-pattern>/*url-pattern>
  filter-mapping>
  <servlet>
    <servlet-name>mvcservlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
    <init-param>
      <param-name>contextConfigLocationparam-name>
      <param-value>classpath:applicationContext-mvc.xmlparam-value>
    init-param>
    <load-on-startup>1load-on-startup>
  servlet>
  <servlet-mapping>
    <servlet-name>mvcservlet-name>
    <url-pattern>/url-pattern>
  servlet-mapping>
web-app>

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title heretitle>
<script type="text/javascript" src="js/jquery.min.js">script>
<script type="text/javascript">

function login(){
	var username = $("#username").val();
	var password = $("#password").val();
	var params = "username="+username+"&password="+password;
	$.ajax({
		'url' : '${pageContext.request.contextPath }/login',
		'data' : params,
		'success' : function(data){
			if(data.code == 200){
				var token = data.token;
				// web storage的查看 - 在浏览器的开发者面板中的application中查看。
				// local storage - 本地存储的数据。 长期有效的。
				// session storage - 会话存储的数据。 一次会话有效。
				var localStorage = window.localStorage; // 浏览器提供的存储空间。 根据key-value存储数据。
				localStorage.token = token;
			}else{
				alert(data.msg);
			}
		}
	});
}

function setHeader(xhr){ // XmlHttpRequest
	xhr.setRequestHeader("Authorization",window.localStorage.token);
}

function testLocalStorage(){
	$.ajax({
		'url' : '${pageContext.request.contextPath}/testAll',
		'success' : function(data){
			if(data.code == 200){
				window.localStorage.token = data.token;
				alert(data.data);
			}else{
				alert(data.msg);
			}
		},
		'beforeSend' : setHeader
	});
}

script>
head>
<body >
	<center>
		<table>
			<caption>登录测试caption>
			<tr>
				<td style="text-align: right; padding-right: 5px">
				登录名:
				td>
				<td style="text-align: left; padding-left: 5px">
				<input type="text" name="username" id="username"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px">
				密码:
				td>
				<td style="text-align: left; padding-left: 5px">
				<input type="text" name="password" id="password"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px" colspan="2">
				<input type="button" value="登录" onclick="login();" />
				td>
			tr>
		table>
	center>
	<input type="button" value="testLocalStorage" onclick="testLocalStorage();"/>
body>
html>

注意

  使用JWT实现单点登录时,需要注意token时效性。token是保存在客户端的令牌数据,如果永久有效,则有被劫持的可能。token在设计的时候,可以考虑一次性有效或一段时间内有效。如果设置有效时长,则需要考虑是否需要刷新token有效期问题。

token保存位置

使用JWT技术生成的token,客户端在保存的时候可以考虑cookie或localStorage。cookie保存方式,可以实现跨域传递数据。localStorage是域私有的本地存储,无法实现跨域。

webstorage

  webstorage可保存的数据容量为5M。且只能存储字符串数据。
  webstorage分为localStorage和sessionStorage。
  localStorage的生命周期是永久的,关闭页面或浏览器之后localStorage中的数据也不会消失。localStorage除非主动删除数据,否则数据永远不会消失。
  sessionStorage是会话相关的本地存储单元,生命周期是在仅在当前会话下有效。sessionStorage引入了一个“浏览器窗口”的概念,sessionStorage是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是sessionStorage在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage也是不一样的。

Restful接口设计

Restful简介

  REST(英文:Representational State Transfer,简称REST)描述了一个架构样式的网络系统,比如 web 应用程序。它首次出现在 2000 年 Roy Fielding 的博士论文中,他是 HTTP 规范的主要编写者之一。在目前主流的三种Web服务交互方案中,REST相比于SOAP(Simple Object Access protocol,简单对象访问协议)以及XML-RPC更加简单明了,无论是对URL的处理还是对Payload的编码,REST都倾向于用更加简单轻量的方法设计和实现。值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。

Restful简述

对应的中文是rest式的;Restful web service是一种常见的rest的应用,是遵守了rest风格的web服务;rest式的web服务是一种ROA(The Resource-Oriented Architecture)(面向资源的架构).

Restful特性

1.普通架构

每次请求的接口或者地址,都在做描述,例如查询的时候用了query,新增的时候用了save。如:
http://127.0.0.1/user/query/1 GET 根据用户id查询用户数据
http://127.0.0.1/user/save POST 新增用户

2.Restful架构

使用get请求,就是查询.使用post请求,就是新增的请求,意图明显,没有必要做描述,这就是restful。
http://127.0.0.1/user/1 GET 根据用户id查询用户数据
http://127.0.0.1/user POST 新增用户

3.Restful操作方式

单点登录课程笔记_第4张图片
幂等性:多次访问,结果资源状态是否相同
安全:访问是否会变更服务器资源状态

delete:只能请求头传参
put:可以请求体传参,但springmvc根本不处理

案例代码

单点登录课程笔记_第5张图片
pom

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
	<groupId>com.sxtgroupId>
	<artifactId>sso-restful-springmvcartifactId>
	<version>1.0version>
	<packaging>warpackaging>
	<dependencies>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-contextartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-webmvcartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		<dependency>
			<groupId>org.springframeworkgroupId>
			<artifactId>spring-aspectsartifactId>
			<version>5.0.6.RELEASEversion>
		dependency>
		<dependency>
			<groupId>javax.servletgroupId>
			<artifactId>jstlartifactId>
			<version>1.2version>
		dependency>
		<dependency>
			<groupId>javax.servletgroupId>
			<artifactId>servlet-apiartifactId>
			<version>2.5version>
			<scope>providedscope>
		dependency>
		<dependency>
			<groupId>javax.servlet.jspgroupId>
			<artifactId>jsp-apiartifactId>
			<version>2.2version>
			<scope>providedscope>
		dependency>
	dependencies>
	<build>
		<pluginManagement>
			<plugins>
				
				<plugin>
					<groupId>org.apache.tomcat.mavengroupId>
					<artifactId>tomcat7-maven-pluginartifactId>
					<version>2.2version>
				plugin>
			plugins>
		pluginManagement>
		<plugins>
			<plugin>
				<groupId>org.apache.tomcat.mavengroupId>
				<artifactId>tomcat7-maven-pluginartifactId>
				<configuration>
					<port>80port>
					<path>/path>
				configuration>
			plugin>
		plugins>
	build>
project>

web.xml
put请求默认不处理请求体


<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>sso-cross-domaindisplay-name>
	<welcome-file-list>
		<welcome-file>index.htmlwelcome-file>
		<welcome-file>index.htmwelcome-file>
		<welcome-file>index.jspwelcome-file>
		<welcome-file>default.htmlwelcome-file>
		<welcome-file>default.htmwelcome-file>
		<welcome-file>default.jspwelcome-file>
	welcome-file-list>
	<filter>
		<filter-name>charSetFilterfilter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
		<init-param>
			<param-name>encodingparam-name>
			<param-value>UTF-8param-value>
		init-param>
	filter>
	
	<filter>
		<filter-name>httpPutFormContentFilterfilter-name>
		<filter-class>org.springframework.web.filter.HttpPutFormContentFilterfilter-class>
	filter>
	<filter-mapping>
		<filter-name>httpPutFormContentFilterfilter-name>
		<url-pattern>/*url-pattern>
	filter-mapping>
	<filter-mapping>
		<filter-name>charSetFilterfilter-name>
		<url-pattern>/*url-pattern>
	filter-mapping>
	<servlet>
		<servlet-name>mvcservlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
		<init-param>
			<param-name>contextConfigLocationparam-name>
			<param-value>classpath:applicationContext-mvc.xmlparam-value>
		init-param>
		<load-on-startup>1load-on-startup>
	servlet>
	<servlet-mapping>
		<servlet-name>mvcservlet-name>
		<url-pattern>/url-pattern>
	servlet-mapping>
web-app>

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title heretitle>
<script type="text/javascript" src="js/jquery.min.js">script>
<script type="text/javascript">

function add(){
	$.ajax({
		'url':'${pageContext.request.contextPath}/user',
		'type':'post',
		'data':'user={id:10,name:test}',
		'success':function(data){
			alert(data);
		}
	});
}

function modify(){
	$.ajax({
		'url':'${pageContext.request.contextPath}/user',
		'type':'put',
		'data':'user={id:1,name:guest}',
		'success':function(data){
			alert(data);
		}
	});
}

function del(){
	$.ajax({
		'url':'${pageContext.request.contextPath}/user?id=3',
		'type':'delete',
		'success':function(data){
			alert(data);
		}
	});
}

function get(){
	$.ajax({
		'url':'${pageContext.request.contextPath}/user/1',
		'type':'get',
		'success':function(data){
			alert(data);
		}
	});
}

script>
head>
<body>
	<center>
	
		<input type="button" value="新增" onclick="add();"/><br>
		<input type="button" value="修改" onclick="modify();"/><br>
		<input type="button" value="删除" onclick="del();"/><br>
		<input type="button" value="查询" onclick="get();"/><br>
	
	center>
body>
html>

applicationContext.xml


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="com.sxt" />
	
	<mvc:annotation-driven />
	
	<mvc:resources location="/js/" mapping="/js/**">mvc:resources>

beans>

TestRestfulController.java

package com.sxt.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sxt.service.TestRestfulService;

@RequestMapping("/user")  
@Controller  
public class TestRestfulController {  
  
    @Autowired  
    private TestRestfulService newUserService;  
  
    /** 
     * 根据用户id查询用户数据 
     *  
     * @param id  path variable参数
     * @return 
     */  
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)  
    @ResponseBody  
    public ResponseEntity<String> queryUserById(@PathVariable("id") Long id) {  
        try {  
        	String user = this.newUserService.queryUserById(id);  
            if (null == user) {  
                // 资源不存在,响应404  
                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);  
            }  
            // 200  
            // return ResponseEntity.status(HttpStatus.OK).body(user);  
            return ResponseEntity.ok(user);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        // 500  
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);  
    }  
  
    /** 
     * 新增用户 
     *  
     * @param user 
     * @return 
     */  
    @RequestMapping(method = RequestMethod.POST)  
    public ResponseEntity<Void> saveUser(String user) {  
        try {  
            this.newUserService.saveUser(user);  
            return ResponseEntity.status(HttpStatus.CREATED).build();  
        } catch (Exception e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
        // 500  
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);  
    }  
  
    /** 
     * 更新用户资源 
     *  
     * @param user 
     * @return 
     */  
    @RequestMapping(method = RequestMethod.PUT)  
    public ResponseEntity<Void> updateUser(String user) {  
        try {  
            this.newUserService.updateUser(user);  
            return ResponseEntity.status(HttpStatus.NO_CONTENT).build();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        // 500  
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);  
    }  
  
    /** 
     * 删除用户资源 
     *  
     * @param user 
     * @return 
     */  
    @RequestMapping(method = RequestMethod.DELETE)  
    public ResponseEntity<Void> deleteUser(@RequestParam(value = "id", defaultValue = "0") Long id) {  
        try {  
            if (id.intValue() == 0) {  
                // 请求参数有误  
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();  
            }  
            this.newUserService.deleteUserById(id);  
            // 204  
            return ResponseEntity.status(HttpStatus.NO_CONTENT).build();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        // 500  
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);  
    }  
}  

TestRestfulService.java

package com.sxt.service;

public interface TestRestfulService {

	String queryUserById(Long id);

	void saveUser(String user);

	void updateUser(String user);

	void deleteUserById(Long id);

}

TestRestfulServiceImpl.java

package com.sxt.service.impl;

import org.springframework.stereotype.Service;

import com.sxt.service.TestRestfulService;

@Service
public class TestRestfulServiceImpl implements TestRestfulService {

	@Override
	public String queryUserById(Long id) {
		System.out.println("queryUserById : " + id);
		return "{id:"+id+", name:admin}";
	}

	@Override
	public void saveUser(String user) {
		System.out.println("saveUser : " + user);
	}

	@Override
	public void updateUser(String user) {
		System.out.println("updateUser : " + user);
	}

	@Override
	public void deleteUserById(Long id) {
		System.out.println("deleteUserById : " + id);
	}

}

响应状态编码

单点登录课程笔记_第6张图片
单点登录课程笔记_第7张图片

接口安全机制

在对外发布服务接口的时候,定制一套签名机制,保证数据传递有效性的。

1安全机制的设计方案

1.1单向加密

  在理论上,从明文加密到密文后,不可反向解密的。
  可以从迭代和加盐的方式尽可能保证加密数据不可反向解密。
  传递敏感数据的时候使用的。如:密码。
在金融相关交易中,用户密码是敏感数据,其他数据是非敏感数据。所有的金融相关的应用中,客户端都有一个独立的密码输入控件。这个控件就是做单向加密的。
使用单向加密的时候,传递的数据只有密文,没有明文,也没有密钥。

1.2双向加密

  是可以实现加密和解密双向运算的算法。需要通过密钥实现加解密计算的。
  密钥种类:公钥、私钥。
    公钥:可以对外公开的,就是可以在网络中传递的。
    私钥:必须保密的,绝对不会对外暴露的。
在传递安全数据的时候使用。所谓安全数据,就是不可篡改的数据。如:金融交易中的收款人卡号,转账的金额,货币的种类等。
使用双向加密的时候,传递的有明文,密文,公钥。

1.2.1对称加密

只有一个密钥,就是公钥。

1.2.2非对称加密

有两个密钥,公钥和私钥。

2DES加密

详见代码
DES的密文是非定长密文。根据明文数据和key数据动态伸缩的。

单点登录课程笔记_第8张图片
des.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>DES算法title>
    <script src="/js/jquery.min.js">script>
    <script src="/js/tripledes.js">script>
    <script src="/js/mode-ecb-min.js">script>
    <script>
	    function uuid() {
			var s = [];
			var hexDigits = "0123456789abcdef";
			for (var i = 0; i < 36; i++) {
			  	s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
			}
			s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
			s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
			s[8] = s[13] = s[18] = s[23] = "-";
			
			var uuid = s.join("");
			return uuid;
    	}
        /*
         * 加密函数
         * message - 要加密的源数据
         * key - 密钥
         */
        function encryptByDES(message, key) {
			// 解析密钥, 将密钥转换成16进制数据。 就是解析为字节数据。
            var keyHex = CryptoJS.enc.Utf8.parse(key);
			// 创建DES加密工具。 构建器。
            var encrypted = CryptoJS.DES.encrypt(message, keyHex, {
                mode: CryptoJS.mode.ECB, // 加密的模式, ECB加密模式。
                padding: CryptoJS.pad.Pkcs7 // 加密的padding
            });
            return encrypted.toString(); // 加密,并获取加密后的密文数据。
        }
		
        /*
         * 解密函数
         * ciphertext - 要解密的密文数据。
         * key - 密钥
         */
        function decryptByDES(ciphertext, key) {

            var keyHex = CryptoJS.enc.Utf8.parse(key);
            // 创建解密工具
            var decrypted = CryptoJS.DES.decrypt({
                ciphertext: CryptoJS.enc.Base64.parse(ciphertext) // 将密文数据解析为可解密的字节数据。
            }, keyHex, {
                mode: CryptoJS.mode.ECB,
                padding: CryptoJS.pad.Pkcs7
            });
            return decrypted.toString(CryptoJS.enc.Utf8); // 解密过程,并返回明文。
        }

        function doPost(){
        	var name = $("#nameText").val();
        	var password = $("#passwordText").val();
        	var message = name + password;
        	var key = uuid();
        	var param = {};
        	param.name=name;
        	param.password=password;
        	param.key=key;
        	// 正确的加密
        	param.message = encryptByDES(message, key);
        	// 测试解密错误,如:请求拦截。
        	// param.message = "WrongSecurityMessage00";
        	// 测试异常情况。DES加密后的密文数据长度一定是8的整数倍。
        	// param.message = "testException";
        	$.ajax({
        		'url':'/testDes',
        		'data':param,
        		'success':function(data){
        			if(data){
        				alert("密文:"+data.securityMessage+";key:"+data.key);
        				var respMsg = decryptByDES(data.securityMessage, data.key);
        				alert(respMsg);
        			}else{
        				alert("服务器忙请稍后重试!");
        			}
        		}
        	});
        }

    script>
head>

<body>
	<center>
		<table>
			<caption>DES安全测试caption>
			<tr>
				<td style="text-align: right; padding-right: 5px">
					姓名:
				td>
				<td style="text-align: left; padding-left: 5px">
					<input type="text" name="name" id="nameText"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px">
					密码:
				td>
				<td style="text-align: left; padding-left: 5px">
					<input type="text" name="password" id="passwordText"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px" colspan="2">
					<input type="button" value="测试" onclick="doPost();" />
				td>
			tr>
		table>
	center>
body>
html>

controller

package com.sxt.des.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sxt.des.utils.DesResponse;
import com.sxt.service.DesService;

@Controller
public class DesController {
	
	@Autowired
	private DesService desService;
	
	/**
	 * 加密逻辑为: name+password 使用key作为密匙源,加密得到securityMessage
	 * @param key 密匙源
	 * @param securityMessage 加密后的签名
	 * @param name 用户名
	 * @param password 密码
	 * @return
	 */
	@RequestMapping("/testDes")
	@ResponseBody
	public DesResponse testDes(@RequestParam("key") String key, @RequestParam("message") String securityMessage
			, @RequestParam("name") String name, @RequestParam("password") String password){
		
		DesResponse resp = this.desService.testDes(key, securityMessage, name, password);
		
		return resp;
	}
	
}

serviceimpl

package com.sxt.service.impl;

import org.springframework.stereotype.Service;

import com.sxt.des.utils.DesCrypt;
import com.sxt.des.utils.DesResponse;
import com.sxt.service.DesService;

@Service
public class DesServiceImpl implements DesService {

	@Override
	public DesResponse testDes(String key, String securityMessage, String name, String password) {
		System.out.println("接收到的密文: " + securityMessage);
		System.out.println("请求参数name:" + name + " ; 请求参数password:" + password);
		System.out.println("请求key: " + key);
		DesResponse resp = new DesResponse();
		String respKey = DesCrypt.getKEY();
		String message = "";
		
		// 解密解析请求数据
		try{
			// 解密得到请求源参数
			String decodeMessage = DesCrypt.decode(key, securityMessage);
			System.out.println("解密后的数据:" + decodeMessage);
			// 校验请求参数
			if(!decodeMessage.equals((name + password))){
				// 请求参数校验失败
				message = "请求数据被篡改!";
			}else{
				// 请求参数验证成功
				message = "登录成功!";
			}
		}catch(Exception e){
			e.printStackTrace();
			// 有解密异常发生。
			message = "请求数据解析错误!";
		}
		
		System.out.println("响应中的key:" + respKey);
		System.out.println("响应消息明文:" + message);
		// 加密处理响应数据
		try{
			message = DesCrypt.encode(respKey, message);
		}catch(Exception e){
			e.printStackTrace();
			// 加密异常发生。
			return null;
		}
		System.out.println("响应消息密文:" + message);
		resp.setKey(respKey);
		resp.setSecurityMessage(message);
		
		return resp;
		
	}

}

utils

package com.sxt.des.utils;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.SecureRandom;

/**
 * 加密解密工具类
 */
public class DesCrypt {
	// 默认的KEY。此密匙应该根据用户推算或记录在物理存储中。
    private static final String KEY = "com.sxt.des";
    // 字符编码。
    private static final String CODE_TYPE = "UTF-8";

    /**
     * DES加密
     * @param datasource 要加密的源数据。
     * @return 加密后的数据。
     */
    public static String encode(String key, String datasource) throws Exception{
    	if(null == key){
    		key = KEY;
    	}
    	// 随机生成器。如果种子一样,则生成的随机信息可推测。
        SecureRandom random = new SecureRandom();
        // 创建DES密匙。依据提供的密匙字符串创建密匙。 密钥源信息。 需要通过密钥工厂再次推算的,才能得到最终的密钥数据。
        DESKeySpec desKey = new DESKeySpec(key.getBytes(CODE_TYPE));
        // 创建一个密匙工厂,然后用它把DESKeySpec转换成SecretKey
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
        SecretKey securekey = keyFactory.generateSecret(desKey);
        // Cipher对象实际完成加密操作
        Cipher cipher = Cipher.getInstance("DES");
        // 用密匙初始化Cipher对象。 Cipher.ENCRYPT_MODE - 加密模式
        cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
        // 现在,获取数据并加密。
        // 加密后的数据不要new String。 java中的字符串对象都是有字符集信息的。
        // java中的UTF8字符集又是长度变化的。一个字符串长度为2~3
        byte[] temp = Base64.encodeBase64(cipher.doFinal(datasource.getBytes()));
        return IOUtils.toString(temp,"UTF-8");
    }

    /**
     * DES解密 
     * @param src 要解密的密文数据
     * @return
     */
    public static String decode(String key, String src) throws Exception {
    	if(null == key){
    		key = KEY;
    	}
        // DES算法要求有一个可信任的随机数源
        SecureRandom random = new SecureRandom();
        // 创建一个DESKeySpec对象
        DESKeySpec desKey = new DESKeySpec(key.getBytes(CODE_TYPE));
        // 创建一个密匙工厂
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
        // 将DESKeySpec对象转换成SecretKey对象
        SecretKey securekey = keyFactory.generateSecret(desKey);
        // Cipher对象实际完成解密操作
        Cipher cipher = Cipher.getInstance("DES");
        // 用密匙初始化Cipher对象。 Cipher.DECRYPT_MODE - 解密模式
        cipher.init(Cipher.DECRYPT_MODE, securekey, random);
        // 真正开始解密操作
        return IOUtils.toString(cipher.doFinal(Base64.decodeBase64(src)),"UTF-8");
    }
    
    public static String getKEY(){
    	return KEY;
    }

}

package com.sxt.des.utils;

public class DesResponse {

	private String key;
	private String securityMessage;
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
	public String getSecurityMessage() {
		return securityMessage;
	}
	public void setSecurityMessage(String securityMessage) {
		this.securityMessage = securityMessage;
	}
	
}

3AES加密

详见代码
AES的key要求长度为16。
aes.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>AES算法title>
    <script src="/js/jquery.min.js">script>
    <script src="/js/aes.min.js">script>
    <script>
    	// 随机数生成算法。 len-生成结果的长度, radix-生成结果的组成,是二进制,十进制还是十六进制数。
	    function uuid(len, radix) {
	        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
	        var uuid = [], i;
	        radix = radix || chars.length;
	     
	        if (len) {
	          // Compact form
	          for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
	        } else {
	          // rfc4122, version 4 form
	          var r;
	     
	          // rfc4122 requires these characters
	          uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
	          uuid[14] = '4';
	     
	          // Fill in random data.  At i==19 set the high bits of clock sequence as
	          // per rfc4122, sec. 4.1.5
	          for (i = 0; i < 36; i++) {
	            if (!uuid[i]) {
	              r = 0 | Math.random()*16;
	              uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
	            }
	          }
	        }
	     
	        return uuid.join('');
	    }
        /*
         * 加密函数
         * message - 明文数据
         * key - 密钥
         */
         function encryptByAES(message, key){
       	    var keyHex = CryptoJS.enc.Utf8.parse(key);
       	    var srcs = CryptoJS.enc.Utf8.parse(message);
       	    var encrypted = CryptoJS.AES.encrypt(srcs, keyHex, {
       	    	mode:CryptoJS.mode.ECB,
       	    	padding: CryptoJS.pad.Pkcs7
       	    });
       	    return encrypted.toString();
       	}
		
        /*
         * 解密函数
         * ciphertext - 要解密的密文。
         */
         function decryptByAES(ciphertext, key){
       	    var keyHex = CryptoJS.enc.Utf8.parse(key);
       	    var decrypt = CryptoJS.AES.decrypt(ciphertext, keyHex, {
       	    	mode:CryptoJS.mode.ECB,
       	    	padding: CryptoJS.pad.Pkcs7
       	    });
       	    return CryptoJS.enc.Utf8.stringify(decrypt).toString();
       	}
        
        function doPost(){
        	var name = $("#nameText").val();
        	var password = $("#passwordText").val();
        	var message = name + password;
        	var key = uuid(32,16);
        	var param = {};
        	param.name=name;
        	param.password=password;
        	param.key=key;
        	// 正确的加密
        	param.message = encryptByAES(message, key);
        	// 测试解密错误,如:请求拦截。
        	// param.message = "WrongSecurityMessage00";
        	// 测试异常情况。AES加密后的密文数据长度一定是8的整数倍。
        	// param.message = "testException";
        	$.ajax({
        		'url':'/testAes',
        		'data':param,
        		'success':function(data){
        			if(data){
        				alert("密文:"+data.securityMessage+";key:"+data.key);
        				var respMsg = decryptByAES(data.securityMessage, data.key);
        				alert(respMsg);
        			}else{
        				alert("服务器忙请稍后重试!");
        			}
        		}
        	});
        }

    script>
head>

<body>
	<center>
		<table>
			<caption>AES安全测试caption>
			<tr>
				<td style="text-align: right; padding-right: 5px">
					姓名:
				td>
				<td style="text-align: left; padding-left: 5px">
					<input type="text" name="name" id="nameText"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px">
					密码:
				td>
				<td style="text-align: left; padding-left: 5px">
					<input type="text" name="password" id="passwordText"/>
				td>
			tr>
			<tr>
				<td style="text-align: right; padding-right: 5px" colspan="2">
					<input type="button" value="测试" onclick="doPost();" />
				td>
			tr>
		table>
	center>
body>
html>

AesController.java

package com.sxt.aes.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sxt.aes.service.AesService;
import com.sxt.aes.utils.AesResponse;

@Controller
public class AesController {
	
	@Autowired
	private AesService aesService;
	
	/**
	 * 加密逻辑为: name+password 使用key作为密匙源,加密得到securityMessage
	 * @param key 密匙源
	 * @param securityMessage 加密后的签名
	 * @param name 用户名
	 * @param password 密码
	 * @return
	 */
	@RequestMapping("/testAes")
	@ResponseBody
	public AesResponse testDes(@RequestParam("key") String key, @RequestParam("message") String securityMessage
			, @RequestParam("name") String name, @RequestParam("password") String password){
		
		AesResponse resp = this.aesService.testAes(key, securityMessage, name, password);
		
		return resp;
	}
	
}

serviceimpl

package com.sxt.aes.service.impl;

import org.springframework.stereotype.Service;

import com.sxt.aes.service.AesService;
import com.sxt.aes.utils.AesCrypt;
import com.sxt.aes.utils.AesResponse;

@Service
public class AesServiceImpl implements AesService {

	@Override
	public AesResponse testAes(String key, String securityMessage, String name, String password) {
		System.out.println("接收到的密文: " + securityMessage);
		System.out.println("请求参数name:" + name + " ; 请求参数password:" + password);
		System.out.println("请求key: " + key);
		AesResponse resp = new AesResponse();
		String respKey = AesCrypt.getKEY();
		String message = "";
		
		// 解密解析请求数据
		try{
			// 解密得到请求源参数
			String decodeMessage = AesCrypt.aesDecrypt(securityMessage, key);
			System.out.println("解密后的数据:" + decodeMessage);
			// 校验请求参数
			if(!decodeMessage.equals((name + password))){
				// 请求参数校验失败
				message = "请求数据被篡改!";
			}else{
				// 请求参数验证成功
				message = "登录成功!";
			}
		}catch(Exception e){
			e.printStackTrace();
			// 有解密异常发生。
			message = "请求数据解析错误!";
		}
		
		System.out.println("响应中的key:" + respKey);
		System.out.println("响应消息明文:" + message);
		// 加密处理响应数据
		try{
			message = AesCrypt.aesEncrypt(message, respKey);
		}catch(Exception e){
			e.printStackTrace();
			// 加密异常发生。
			return null;
		}
		System.out.println("响应消息密文:" + message);
		resp.setKey(respKey);
		resp.setSecurityMessage(message);
		
		return resp;
		
	}

}

utils

package com.sxt.aes.utils;

import java.math.BigInteger;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

import sun.misc.BASE64Decoder;

/**
 * AES的加密和解密
 * @author libo
 */
public class AesCrypt {
    // 密钥 ,长度是16
    private static final String KEY = "com.sxt.aes.keys";  
    // 算法
    private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";
    // private static final String ALGORITHMSTR = "AES/CBC/PKCS5Padding";
  
    /** 
     * 将byte[]转为各种进制的字符串 
     * @param bytes byte[] 
     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 
     * @return 转换后的字符串 
     */  
    public static String binary(byte[] bytes, int radix){  
        return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数  
    }  
  
    /** 
     * base 64 encode 
     * @param bytes 待编码的byte[] 
     * @return 编码后的base 64 code 
     */  
    public static String base64Encode(byte[] bytes){  
        return Base64.encodeBase64String(bytes);  
    }  
  
    /** 
     * base 64 decode 
     * @param base64Code 待解码的base 64 code 
     * @return 解码后的byte[] 
     * @throws Exception 
     */  
    public static byte[] base64Decode(String base64Code) throws Exception{  
        return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);  
    }  
  
      
    /** 
     * AES加密 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的byte[] 
     * @throws Exception 
     */  
    public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {  
    	// 密钥生成器
        // KeyGenerator kgen = KeyGenerator.getInstance("AES");  
        // 密钥生成器初始化, 密钥生成器初始化,会影响到Cipher的处理。
        // kgen.init(128);  
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);  
        
        /*// AES/CBC/PKCS5Padding 算法模式为CBC可以增加偏移量,可增加加密算法强度。
        String ivParameter = "0392039203920300";
        IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"), iv);  
        */
        
        // 初始化, Cipher.ENCRYPT_MODE-加密模式, SecretKeySpec-具体的密钥
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));  
  
        return cipher.doFinal(content.getBytes("utf-8"));  
    }  
  
  
    /** 
     * AES加密为base 64 code 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的base 64 code 
     * @throws Exception 
     */  
    public static String aesEncrypt(String content, String encryptKey) throws Exception {  
        return base64Encode(aesEncryptToBytes(content, encryptKey));  
    }  
  
    /** 
     * AES解密 
     * @param encryptBytes 待解密的byte[] 
     * @param decryptKey 解密密钥 
     * @return 解密后的String 
     * @throws Exception 
     */  
    public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {  
       /* KeyGenerator kgen = KeyGenerator.getInstance("AES");  
        kgen.init(128);  */
  
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);  
        // Cipher.DECRYPT_MODE - 解密模式
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));  
        byte[] decryptBytes = cipher.doFinal(encryptBytes);  
        return new String(decryptBytes);  
    }  
  
  
    /** 
     * 将base 64 code AES解密 
     * @param encryptStr 待解密的base 64 code 
     * @param decryptKey 解密密钥 
     * @return 解密后的string 
     * @throws Exception 
     */  
    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {  
        return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);  
    }   
    
    public static String getKEY(){
    	return KEY;
    }
    
    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {  
        String content = "testtest";  
        System.out.println("加密前:" + content);  
        System.out.println("加密密钥和解密密钥:" + KEY);  
        String encrypt = aesEncrypt(content, KEY);  
        System.out.println("加密后:" + encrypt);  
        String decrypt = aesDecrypt(encrypt, KEY);  
        System.out.println("解密后:" + decrypt);  
    } 
}

package com.sxt.aes.utils;

public class AesResponse {

	private String key;
	private String securityMessage;
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
	public String getSecurityMessage() {
		return securityMessage;
	}
	public void setSecurityMessage(String securityMessage) {
		this.securityMessage = securityMessage;
	}
	
}

4使用场景

DES和AES在使用场景上没有区别。
传递非敏感的安全性数据可以使用。如:QQ通讯录获取,微信中的消息传递。

5对比

DES - 加密后的数据是16的整数倍。 是16字节整数倍。
AES - 要求key的长度必须是16字节。 AES相对效率较低,但是可以通过偏移量强化加密。

你可能感兴趣的:(⑧,实用开发,spring)