# 单点登录(sso)
<div class="note info">一处登录,处处使用</div>
<div class="note primary no-icon">对单点登录的学习及使用</div>
### 有关时序图:

## cookie和session的区别
| 区别 | cookie | session |
| :------: | :-------------------: | :------: |
| 存储位置 | 客户端 | 服务器 |
| 安全性 | 不安全 | 安全 |
| 数据类型 | 保存大小(4k) String | Object |
| 生命周期 | setMaxAge() | 一次会话 |
### :rocket:使用技术:cookie+redis+JWT+拦截器+自定义注解+httpClientUtils
## UserInfoServiceImpl核对后台登录信息+用户登录信息载入缓存
### controller
```java
@Controller
public class PassportController {
@Reference
private UserService userService;
@RequestMapping("index")
public String index(HttpServletRequest request) {
String originUrl = request.getParameter("originUrl");
request.setAttribute("originUrl",originUrl);
return "index";
}
@RequestMapping("login")
@ResponseBody
public String login(UserInfo userInfo) {
//调用登录方法
UserInfo info = userService.login(userInfo);
if (info != null) {
return "success";
}else {
return "fail";
}
}
}
```
### UserServiceImpl
```java
@Service
public class UserServiceImpl implements UserService {
public String userKey_prefix="user:";
public String userinfoKey_suffix=":info";
public int userKey_timeOut=60*60*24;
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private UserAdderssMapoper userAdderssMapoper;
@Autowired
private RedisUtil redisUtil;
@Override
public List<UserInfo> findAll() {
return userInfoMapper.selectAll();
}
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress userAddress = new UserAddress();
userAddress.setId(userId);
return userAdderssMapoper.select(userAddress);
}
@Override
public UserInfo login(UserInfo userInfo) {
//密码
String passwd = userInfo.getPasswd();
String digestAsHex = DigestUtils.md5DigestAsHex(passwd.getBytes());
userInfo.setPasswd(digestAsHex);
//登录
UserInfo info = userInfoMapper.selectOne(userInfo);
if (info != null) {
Jedis jedis = redisUtil.getJedis();
String userKey = userKey_prefix+info.getId()+userinfoKey_suffix;
jedis.setex(userKey,userKey_timeOut,JSON.toJSONString(info));
jedis.close();
return info;
}
return null;
}
}
```
## 生成Token
### 什么是JWT
JWT(Json Web Token) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
JWT 最重要的作用就是对 token信息的**防伪**作用。
JWT的原理:
一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到 JWT。

### 使用步骤
1. 在pom.xml中添加
```java
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
```
2. JWTUtils
```java
public class JwtUtil {
public static String encode(String key,Map<String,Object> param,String salt){
if(salt!=null){
key+=salt;
}
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256,key);
jwtBuilder = jwtBuilder.setClaims(param);
String token = jwtBuilder.compact();
return token;
}
public static Map<String,Object> decode(String token , String key, String salt){
Claims claims=null;
if (salt!=null){
key+=salt;
}
try {
claims= Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
} catch ( JwtException e) {
return null;
}
return claims;
}
}
```
- 测试
```java
@Test
public void testJwt() {
String key = "atguan";
Map<String,Object> map = new HashMap<>();
String salt = "192.168.113.1";
map.put("userId",1001);
map.put("nickName","admin");
String token = JwtUtil.encode(key, map, salt);
System.err.println(token);
//解密
Map<String, Object> decode = JwtUtil.decode(token, key, salt);
System.err.println(decode);
}
```
## JWT实战
- 完善login控制器
```java
@RequestMapping("login")
@ResponseBody
public String login(UserInfo userInfo,HttpServletRequest request) {
//salt服务器ip地址
String salt = request.getHeader("X-forwarded-for");
//调用登录方法
UserInfo info = userService.login(userInfo);
if (info != null) {
//制作token
Map<String,Object> map = new HashMap<>();
map.put("userId",userInfo.getId());
map.put("nickName",userInfo.getNickName());
String token = JwtUtil.encode(key, map, salt);
System.err.println(token);
return token;
}else {
return "fail";
}
}
```
### 认证verify
1. controller
```java
@RequestMapping("verify")
@ResponseBody
public String verify(HttpServletRequest request) {
// String salt = request.getHeader("X-forwarded-for");
//获取token
String token = request.getParameter("token");
String salt = request.getParameter("salt");
//调用jwt
Map<String, Object> map = JwtUtil.decode(token, key, salt);
if (map != null && map.size() > 0) {
String userId = (String) map.get("userId");
UserInfo userInfo = userService.verify(userId);
if (userInfo != null) {
return "success";
}else {
return "fail";
}
}
return "fail";
}
```
2. service
```java
@Override
public UserInfo verify(String userId) {
Jedis jedis = null;
try {
jedis = redisUtil.getJedis();
String userKey = userKey_prefix+userId+userinfoKey_suffix;
String userJson = jedis.get(userKey);
if (!StringUtils.isEmpty(userJson)) {
UserInfo userInfo = JSON.parseObject(userJson, UserInfo.class);
return userInfo;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return null;
}
```
### 自定义拦截器
<div class="note info no icon">在每次访问控制器时自动拦截,检查是否携带token或cookie中存在token</div>
- cookie工具类
```java
public class CookieUtil {
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals(cookieName)) {
if (isDecoder) {//如果涉及中文
retValue = URLDecoder.decode(cookies[i].getValue(), "UTF-8");
} else {
retValue = cookies[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage >= 0)
cookie.setMaxAge(cookieMaxage);
if (null != request)// 设置域名的cookie
cookie.setDomain(getDomainName(request));
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
System.out.println("domainName = " + domainName);
return domainName;
}
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
setCookie(request, response, cookieName, null, 0, false);
}
}
```
- WebConst
```java
public class WebConst {
public static final int COOKIE_MAXAGE=7*24*3600;
public static final String VERIFY_ADDRESS="http://passport.atguan.com/verify";
public static final String LOGIN_ADDRESS="http://passport.atguan.com/index";
}
```
- AuthInterceptor拦截器
```java
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("newToken");
if (token != null) {
CookieUtil.setCookie(request,response,"token",token,WebConst.COOKIE_MAXAGE,false);
}
if (token == null) {
token = CookieUtil.getCookieValue(request, "token", false);
}
if (token != null) {
Map<String,Object> map = getUserMapByToken(token);
String nickName = (String) map.get("nickName");
request.setAttribute("nickName",nickName);
}
return true;
}
/**
* 获取map数据
* @param token
* @return
*/
private Map<String, Object> getUserMapByToken(String token) {
//截取中间部分
String userInfo = StringUtils.substringBetween(token, ".");
//base64解码
Base64UrlCodec base64UrlCodec = new Base64UrlCodec();
byte[] decode = base64UrlCodec.decode(userInfo);
//将decode转成string
String mapJson = null;
try {
mapJson = new String(decode, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//将字符串转化为map
return JSON.parseObject(mapJson,Map.class);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
```
- 加入拦截器的配置
```java
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private AuthInterceptor authInterceptor;
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置拦截器
registry.addInterceptor(authInterceptor).addPathPatterns("/**");
//添加拦截器
super.addInterceptors(registry);
}
}
```
- 添加扫描包
```java
@ComponentScan(basePackages = "com.atguan.gmall")
```
### 为方便使用应该自定义一个注解,当有需要时在方法上加上此注解,则会被拦截器拦截
- 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
- ```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequire {
boolean autoRedirect() default true;
}
```
- 完善拦截器,在preHandle方法中加入:
```java
//在拦截器中获取方法上的注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取方法上的注解
LoginRequire methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequire.class);
if (methodAnnotation != null) {
//有注解
//判断用户是否登录,调用verify方法
//获得服务器的ip地址
String salt = request.getHeader("X-forwarded-for");
String result = HttpClientUtil.doGet(WebConst.VERIFY_ADDRESS+"?token="+token+"&salt="+salt);
if ("success".equals(result)) {
//认证成功
//开始解密token
Map map = getUserMapByToken(token);
//取出userId
String userId = (String) map.get("userId");
//保存到作用域
request.setAttribute("userId",userId);
return true;
}else {
//认证失败
if (methodAnnotation.autoRedirect()) {
//表示必须登录
//获取url
String requestUrl = request.getRequestURL().toString();
System.err.println(requestUrl);
//进行转码
String encode = URLEncoder.encode(requestUrl, "UTF-8");
System.err.println(encode);
//拼接重定向
response.sendRedirect(WebConst.LOGIN_ADDRESS+"?originUrl="+encode);
return false;
}
}
```
- HttpClientUtil工具类
```java
public class HttpClientUtil {
public static String doGet(String url) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
EntityUtils.consume(entity);
httpclient.close();
return result;
}
httpclient.close();
}catch (IOException e){
e.printStackTrace();
return null;
}
return null;
}
public static void download(String url,String fileName) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
// String result = EntityUtils.toString(entity, "UTF-8");
byte[] bytes = EntityUtils.toByteArray(entity);
File file =new File(fileName);
// InputStream in = entity.getContent();
FileOutputStream fout = new FileOutputStream(file);
fout.write(bytes);
EntityUtils.consume(entity);
httpclient.close();
fout.flush();
fout.close();
return ;
}
httpclient.close();
}catch (IOException e){
e.printStackTrace();
return ;
}
return ;
}
}
```
## 涉及cookie跨域问题
<div class="note warning">注意cookie工具类中getDomainName方法</div>
- ```java
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
System.out.println("domainName = " + domainName);
return domainName;
}
```

单点登录