一:Session跨域
所谓Session跨域就是摒弃了系统(Tomcat)提供的Session(jsessionid是和servlet绑定的httpsession的唯一标记),而使用自定义的类似Session的机制来保存客户端数据的一种解决方案。如使用cookie跨域共享:通过设置cookie的domain来实现cookie的跨域传递。在cookie中传递一个自定义的session_id。这个session_id是客户端的唯一标记。将这个标记作为key,将客户端需要保存的数据作为value,在服务端进行保存(数据库保存或NoSQL保存)。这种机制就是Session的跨域解决。
跨域: 客户端请求的时候,请求的服务器,不是同一个IP,端口,域名,主机名的时候,都称为跨域。
域:在应用模型,一个完整的,有独立访问路径的功能集合称为一个域。如:百度称为一个应用或系统。百度下有若干的域,如:搜索引擎(www.baidu.com),百度贴吧(tie.baidu.com),百度知道(zhidao.baidu.com),百度地图(map.baidu.com)等。域信息,有时也称为多级域名。域的划分: 以IP,端口,域名,主机名为标准,实现划分。
new Cookie("", "") //新建cookie
request.getCookies() -> cookie[] // 迭代找到需要使用的cookie
response.addCookie() //向浏览器添加cokie
cookie.setDomain() // 为cookie设定有效域范围。
cookie.setPath() //为cookie设定有效URI范围。
二:Spring Session跨域
1:概念
spring-session技术是spring提供的用于处理集群会话共享的解决方案。spring-session技术是将用户session数据保存到三方存储容器中,如:mysql,redis等。Spring-session技术是解决同域名下的多服务器集群session共享问题的。不能解决跨域session共享问题。
使用: 配置一个Spring提供的Filter,实现数据的拦截保存,并转换为spring-session需要的会话对象。必须提供一个数据库的表格信息(由spring-session提供,找spring-session-jdbc.jar/org/springframework/session/jdbc/*.sql,根据具体的数据库找对应的SQL文件,做表格的创建)。
spring-session表:保存客户端session对象的表格。
spring-session-attributes表:保存客户端session中的attributes属性数据的表格。
spring-session框架,是结合Servlet技术中的HTTPSession完成的会话共享机制。在代码中是直接操作HttpSession对象的。
2:主要配置信息
pom.xml
<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.0</modelVersion>
<groupId>hhxy</groupId>
<artifactId>sso-spring-session</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<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-domain</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<filter>
<filter-name>charSetFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charSetFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
applicationContext-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<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="hhxy.controller" />
<!-- 为SpringMVC配置注解驱动 -->
<mvc:annotation-driven />
<!-- 为Spring基础容器开启注解配置信息 -->
<context:annotation-config />
<!-- 就是用于提供HttpSession数据持久化操作的Bean对象。
对象定义后,可以实现数据库相关操作配置,自动的实现HttpSession数据的持久化操作(CRUD)
-->
<bean
class="org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/testspringsession?useUnicode=true&characterEncoding=UTF8"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- 事务管理器。为JdbcHttpSessionConfiguration提供的事务管理器。 -->
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>
</beans>
3:执行流程
三: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;
}
}
四:Token机制
1:传统身份认证
HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。
这种认证中出现的问题是:
Session:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
可扩展性:在服务端的内存中使用Session存储登录信息,伴随而来的是可扩展性问题。
CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展性是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
2:Token身份认证
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:客户端使用用户名、密码请求登录,服务端收到请求,去验证用户名、密码,验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端,客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 、Session Storage里客户端每次向服务端请求资源的时候需要带着服务端签发的 Token,服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。
使用Token验证的优势:
无状态、可扩展:在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
安全性:请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
五、 JSON Web Token(JWT)机制
JWT是一种紧凑且自包含的,用于在多方传递JSON对象的技术。传递的数据可以使用数字签名增加其安全行。可以使用HMAC加密算法或RSA公钥/私钥加密方式。
紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。
自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能。
JWT一般用于处理用户身份验证或数据信息交换。
用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据签名来保证数据的有效性和安全性。
1:JWT数据结构
JWT的数据结构是 : A.B.C。 由字符点‘.’来分隔三部分数据。
A - header 头信息
B - payload (有效荷载?)
C - Signature 签名
1.1:header
数据结构: {“alg”: “加密算法名称”, “typ” : “JWT”}
alg是加密算法定义内容,如:HMAC SHA256 或 RSA
typ是token类型,这里固定为JWT。
1.2:payload
在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。
payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。
公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。公开数据和私有数据可以由程序员任意定义。
注意:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。
1.3:Signature
签名信息。这是一个由开发者提供的信息。是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据的之前。先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head.加密后的payload。再使用相同的加密算法,对加密后的数据和签名信息进行加密。得到最终结果。
2:JWT执行流程
六:基于JWT机制的单点登录
1:核心代码
1.1:前端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 here</title>
<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>
1.2:后端核心代码
package hhxy.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;
}
}
}
2:注意
使用JWT实现单点登录时,需要注意token时效性。token是保存在客户端的令牌数据,如果永久有效,则有被劫持的可能。token在设计的时候,可以考虑一次性有效或一段时间内有效。如果设置有效时长,则需要考虑是否需要刷新token有效期问题。
3:token保存位置
使用JWT技术生成的token,客户端在保存的时候可以考虑cookie或localStorage。cookie保存方式,可以实现跨域传递数据。localStorage是域私有的本地存储,无法实现跨域。
4:webstorage
webstorage可保存的数据容量为5M。且只能存储字符串数据。webstorage分为localStorage和sessionStorage。
localStorage的生命周期是永久的,关闭页面或浏览器之后localStorage中的数据也不会消失。localStorage除非主动删除数据,否则数据永远不会消失。
sessionStorage是会话相关的本地存储单元,生命周期是在仅在当前会话下有效。sessionStorage引入了一个“浏览器窗口”的概念,sessionStorage是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是sessionStorage在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage也是不一样的。
七:Restful接口设计
1:Rest简述
REST(英文:Representational State Transfer,简称REST)描述了一个架构样式的网络系统,比如 web 应用程序。它首次出现在 2000 年 Roy Fielding 的博士论文中,他是 HTTP 规范的主要编写者之一。在目前主流的三种Web服务交互方案中,REST相比于SOAP(Simple Object Access protocol,简单对象访问协议)以及XML-RPC更加简单明了,无论是对URL的处理还是对Payload的编码,REST都倾向于用更加简单轻量的方法设计和实现。值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。
2 Restful简述
对应的中文是rest式的;Restful web service是一种常见的rest的应用,是遵守了rest风格的web服务;rest式的web服务是一种ROA(The Resource-Oriented Architecture)(面向资源的架构).
3 Restful特性
3.1 普通架构
每次请求的接口或者地址,都在做描述,例如查询的时候用了query,新增的时候用了save。如:
http://127.0.0.1/user/query/1 GET 根据用户id查询用户数据
http://127.0.0.1/user/save POST 新增用户
3.2 Restful架构
使用get请求,就是查询.使用post请求,就是新增的请求,意图明显,没有必要做描述,这就是restful:
http://127.0.0.1/user/1 GET 根据用户id查询用户数据
http://127.0.0.1/user POST 新增用户
3.3 :Restful操作方式
HTTP方法 | 资源操作 | 幂等性 | 是否安全 |
GET | 查询 | 是 | 是 |
POST | 新增 | 否 | 否 |
PUT | 修改 | 是 | 否 |
DELETE | 删除 | 是 | 否 |
注:幂等性:多次访问,结果资源状态是否相同。安全:访问是否会变更服务器资源状态
3.4:响应状态码
编码 | HTTP方法 | 响应体内容 | 描述 |
200 | get/put | 资源数据 | 操作成功 |
201 | post | 源数据 | 创建成功 |
202 | post/put/delete | 无 | 请求已接受 |
204 | delete/put | 无 | 请求已处理,无返回数据 |
301 | get | link 链接 | 资源已被移除 |
303 | get | link | 重定向 |
304 | get | 无 | 资源没有被修改 |
400 | get/post/put/delete | 错误提示消息 | 参数错误(缺少,格式错误等) |
401 | get/post/put/delete | 错误提示消息 | 未授权 |
403 | get/post/put/delete | 错误提示消息 | 访问受限、授权过期 |
404 | get/post/put/delete | 错误提示消息 | 资源、服务未找到 |
405 | get/post/put/delete | 错误提示消息 | 不允许的HTTP方法 |
409 | get/post/put/delete | 错误提示消息 | 资源冲突或资源被锁定 |
415 | get/post/put/delete | 错误提示消息 | 不支持的数据类型或媒体类型 |
429 | get/post/put/delete | 错误提示消息 | 请求过多被限制 |
500 | get/post/put/delete | 错误提示消息 | 系统错误 |
501 | get/post/put/delete | 错误提示消息 | 接口未实现 |
4:基于SpringMVC的Restful核心代码
package hhxy.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);
}
}
八:接口安全机制
在对外发布服务接口的时候,定制一套签名机制,保证数据传递有效性的。
1: 安全机制的设计方案
1.1:单向加密
在理论上,从明文加密到密文后,不可反向解密的。可以从迭代和加盐的方式尽可能保证加密数据不可反向解密。传递敏感数据的时候使用的。如:密码。在金融相关交易中,用户密码是敏感数据,其他数据是非敏感数据。所有的金融相关的应用中,客户端都有一个独立的密码输入控件。这个控件就是做单向加密的。
使用单向加密的时候,传递的数据只有密文,没有明文,也没有密钥。
1.2:双向加密
是可以实现加密和解密双向运算的算法。需要通过密钥实现加解密计算的。
密钥种类:公钥、私钥。
公钥:可以对外公开的,就是可以在网络中传递的。
私钥:必须保密的,绝对不会对外暴露的。
在传递安全数据的时候使用。所谓安全数据,就是不可篡改的数据。如:金融交易中的收款人卡号,转账的金额,货币的种类等。
使用双向加密的时候,传递数据可以有明文,密文,公钥。
1.2.1:对称加密
只有一个密钥,就是私钥。
1.2.2 非对称加密
有两个密钥,公钥和私钥。
2:DES加密
DES的密文是非定长密文。根据明文数据和key数据动态伸缩的。
2.1:核心代码
前端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>
后端核心代码
package hhxy.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 = "hhxy";
// 字符编码。
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;
}
}
3:AES加密
AES的key要求长度为16。
3.1:核心代码
前端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>
后端核心代码
package hhxy.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 = "hhxy";
// 算法
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);
}
}
4 使用场景
DES和AES在使用场景上没有区别。传递非敏感的安全性数据可以使用。如:QQ通讯录获取,微信中的消息传递。
5 对比
DES - 加密后的数据是16的整数倍。 是16字节整数倍。
AES - 要求key的长度必须是16字节。 AES相对效率较低,但是可以通过偏移量强化加密。