SSO-单点登录(1)
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; } }
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; } }
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; } }
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; } }
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; } } }
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; } }
<?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="com.sxt.sso.controller" /> <mvc:annotation-driven /> <mvc:resources location="/js/" mapping="/js/**"></mvc:resources> </beans>
<?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>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>
<%@ 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>
<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>com.sxt</groupId> <artifactId>sso-jwt</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</groupId> <artifactId>spring-aspects</artifactId> <version>5.0.6.RELEASE</version> </dependency> <!-- JWT核心依赖 --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.3.0</version> </dependency> <!-- java开发JWT的依赖jar包。 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- 给springmvc提供的响应扩展。@ResponseBody --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.5</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> </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>
http://localhost:8080/sso-jwt/login?username=admin1&password=password1
request
respons
{
"code":200,
"data":null,
"msg":"登录成功",
"token":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjOTY0NjAxMS04ZmU5LTQxZjItYmExYy0wM2Q4MjQ2ZTA4MjEiLCJpc3MiOiJzeHQtdGVzdC1qd3QiLCJzdWIiOiJ7XCJ1c2VybmFtZVwiOlwiYWRtaW4xXCJ9IiwiaWF0IjoxNTQwOTk0Mjc4LCJleHAiOjE1NDA5OTQzMzh9.9bIBYXjHiG1MXvhM8BQ_-pAY1Pk-46a0R2CtFOzYq6I"
}
2. http://localhost:8080/sso-jwt/testAll
respons
{
"code":200,
"data":"{"username":"admin1"}",
"msg":null,
"token":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjOTY0NjAxMS04ZmU5LTQxZjItYmExYy0wM2Q4MjQ2ZTA4MjEiLCJpc3MiOiJzeHQtdGVzdC1qd3QiLCJzdWIiOiJ7XCJ1c2VybmFtZVwiOlwiYWRtaW4xXCJ9IiwiaWF0IjoxNTQwOTk0MjgxLCJleHAiOjE1NDA5OTQzNDF9.h3-5CpljLTpd1f5STTv1jDUI4Y5k9AfLtZpgNairfIk"
}