分布式系统的统一身份认证与单点登录实践

本文实现的方式为:cookie+jwt+redis,先来一张图

现在我以传统的springmvc+jsp的方式来演示整个运转流程,首先创建认证中心

//认证中心的Controller,地址为:http://sso.fg.cn:8080/sso/
@Controller
public class SSOController {
	
	@Autowired
	HttpServletRequest request;
	
	//to 登录
	@RequestMapping("/index")
    public String index(){
        String redirectUrl = request.getParameter("redirectUrl"); //获取登录后重定向的url
        request.setAttribute("redirectUrl",redirectUrl);
        return "index"; //渲染登录页
    }
	
	// do 登录
	@RequestMapping("/login")
	@ResponseBody
	public String login(String username,String password){
		//模拟登录成功
		if ("admin".equals(username) && "admin".equals(password)) {
			
			//存入redis,key=user:admin:info value=admin,过期时间1个小时
			Jedis jedis = new Jedis("192.168.1.3",6379);
			jedis.setex("user:admin:info", 3600, "admin");
			jedis.close();
			
			//使用jwt生成token
			Map<String, Object> map = new HashMap<String, Object>();
			map.put("username", "admin");
			System.out.println(GenericUtil.getIpAddr(request));
			return JwtUtil.encode("sso.fg.cn", map, "salt");
		}
		return "fail";
	}
	
	// 认证方法,校验真实性
    @RequestMapping("/verify")
    @ResponseBody
    public String verify(){
        String token = request.getParameter("token"); //获取token
        if(token != null){
        	Map<String, Object> map = JwtUtil.decode(token, "sso.fg.cn", "salt");
			if (map != null && map.size() > 0) { //解密成功
                String username = (String) map.get("username");
                //查看redis中是否存在该token
            	Jedis jedis = new Jedis("192.168.1.3",6379);
            	String userinfo = jedis.get("user:" + username + ":info");
            	jedis.expire("user:" + username + ":info", 3600);  //重新设置过期时间
    			jedis.close();
    			if(!StringUtils.isEmpty(userinfo)){ //不为空则返回成功
            		return "success";
            	}
            }
        }
        return "fail"; //为空说明要么token是伪造的,要么过期了,返回失败
    }

}
//关于jwt这里不做介绍
public class JwtUtil {
	
	/**
	 * 加密
	 * key:秘钥
	 * param:存放用户信息
	 * salt:盐值
	 */
	public static String encode(String key, Map<String, Object> param, String salt) {
		if (salt != null) {
			key += salt;
		}
		String token = null;
		try {
			token = Jwts.builder().signWith(SignatureAlgorithm.HS256, key.getBytes("utf-8")).setClaims(param).compact();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} //制作token
        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.getBytes("utf-8")).parseClaimsJws(token).getBody();
        } catch ( Exception e) {
            return null;
        }
        return  claims;
    }

}
<!-- 认证中心的统一登录页面 -->

<form>
用户名:<input type="text" name="username" value="admin" /><br>
密码:<input type="password" name="password" value="admin" /><br>
<button type="button" οnclick="login()">登录</button>
</form>

<script src="<%=path %>/js/jquery-2.1.4.min.js"></script>
<script>
function login(){
	$.ajax({
        type: 'POST',
        url: '<%=path %>/login.do',
		data : $("form").serialize(),
		dataType : 'text',
		success : function(result) {
			if(result != "fail"){
				window.location= "${redirectUrl}?newToken=" + result; //登录成功后跳会原页面
			}
		},
		error : function() {
			alert("出错了");
		}
	});
}
</script>

认证中心创建完毕,现在创建web01

package cn.fg.controller;

import java.io.UnsupportedEncodingException;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

import com.alibaba.fastjson.JSON;

import io.jsonwebtoken.impl.Base64UrlCodec;

//web01的Controller,地址为:http://web01.fg.cn:8081/web01/
@Controller
public class Web01Controller {
	
	@Autowired
	HttpServletRequest request;
	
	@Autowired
	HttpServletResponse response;
	
	//通过认证中心登录页,成功登录后的重定向地址
	@RequestMapping("/index")
	public String index(String newToken) {
		//newToken表示是从认证中心的登录页面过来的
		if(newToken != null){
			//直接从token串中拿出用户数据,考虑更安全的话需要调用认证中心的认证方法进行校验
			Map<String, Object> map = this.getUserinfoByToken(newToken);   
			if (map != null && map.size() != 0) { // 成功取出用户信息
				request.setAttribute("userinfo", map); // 用来页面显示用户信息
				// 设置cookie以便其他模块单点登陆
				Cookie cookie = new Cookie("token", newToken);
				cookie.setHttpOnly(true);
				cookie.setPath("/");
				cookie.setDomain(".fg.cn");
				response.addCookie(cookie);
			}
		}
		
		return "index";
	}
	
    private Map<String, Object> getUserinfoByToken(String token) {
        // eyJhbGciOiJIUzI1NiJ9.eyJuaWNrTmFtZSI6Im1hcnJ5IiwidXNlcklkIjoiMTAwMSJ9.TF1RTg_1TnkPNOAkA4Gq549iqwzsBplgeabpHvW15ng
        String tokenUserInfo = org.apache.commons.lang3.StringUtils.substringBetween(token, "."); //两点之间的就是用户信息
        io.jsonwebtoken.impl.Base64UrlCodec base64UrlCodec = new Base64UrlCodec();
        byte[] bytes = base64UrlCodec.decode(tokenUserInfo);
        String str = null;
        try {
            str =  new String(bytes ,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return JSON.parseObject(str, Map.class);
    }
}
<!--web01首页,我们就从这里开始访问http://web01.fg.cn:8081/web01/page/index.jsp -->

<h1>web01</h1>
<c:if test="${empty userinfo }">
<a href="javascript:void(0)" οnclick="login()">登录</a>&nbsp;|  <!-- 没取到用户信息显示登录 -->
</c:if>
<c:if test="${not empty userinfo }">
您好,${userinfo.username },欢迎登陆, <!-- 取到用户信息了 -->
</c:if>
&nbsp;<a href="http://web02.fg.cn:8082/web02/myOrder.do">我的订单</a><br> <!-- web02站点 -->

<script>
<!-- 访问认证中心的统一登录页,带上重定向地址 -->
function login(){
	window.location = "http://sso.fg.cn:8080/sso/index.do?redirectUrl=" + encodeURIComponent("http://web01.fg.cn:8081/web01/index.do");
}
</script>

web01创建完毕,现在创建web02站点

package cn.fg.controller;

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.Consts;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.fluent.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import cn.fg.util.JwtUtil;

//web02的Controller 地址:http://web02.fg.cn:8082/web02/
@Controller
public class Web02Controller {
	
	@Autowired
	HttpServletRequest request;
	
	@Autowired
	HttpServletResponse response;
	
	//to 我的订单
	@RequestMapping("/myOrder")
	public String myOrder() throws ClientProtocolException, IOException {
		
		//查看cookie中是否有cookie,有则说明有其他系统登录过了
		String token = null;
		Cookie[] cookies = request.getCookies();
		if(cookies != null){
			for (Cookie cookie : cookies) {
				if("token".equals(cookie.getName())){
					token = cookie.getValue();
					break;
				}
			}
		}
		
		if(token == null){
			token = request.getParameter("newToken");  //如果存在newToken说明是登录页面过来的
		}
		
		if(token != null){
			//我认为该功能比较重要,需要调用认证中心进行校验
			String result = Request.Get("http://sso.fg.cn:8080/sso/verify.do?token=" + token).execute().returnContent().asString(Consts.UTF_8);
			if ("success".equals(result)) {
				Map<String, Object> userinfo = JwtUtil.decode(token); //使用jwt解密
				//设置cookie以便其他模块单点登陆
				Cookie cookie = new Cookie("token", token);
				cookie.setHttpOnly(true);
				cookie.setPath("/");
				cookie.setDomain(".fg.cn");
				response.addCookie(cookie);
				request.setAttribute("userinfo", userinfo); //显示用户信息
				return "order"; //渲染订单页
			}
		}
		
		//没拿到token,去登录
		return "redirect:http://sso.fg.cn:8080/sso/index.do?redirectUrl=" + request.getRequestURL().toString();
	}
}
<!-- web02订单页 -->

<h1>web02</h1>
<c:if test="${not empty userinfo }">
您好,${userinfo.username },欢迎访问订单系统。
</c:if>

 到此代码创建完成,我们来看看效果

以上只是一个演示,提供思路,在实际开发中,cookie、token的校验应该用拦截器来实现,通过自定义注解,来确定请求该方法前需不要登录

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequire {
    // 默认设置
    boolean autoRedirect() default true;
}
public class AuthInterceptor extends HandlerInterceptorAdapter {
    // preHandle 进入控制器之前。
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 将生产token 放入cookie 中
        // http://item.gmall.com/32.html?newToken=eyJhbGciOiJIUzI1NiJ9.eyJuaWNrTmFtZSI6IkFkbWluaXN0cmF0b3IiLCJ1c2VySWQiOiIyIn0.WUvbFvXQnTMBGNyHWT-DE41MR9cn7c_W1oAtDAzb7VU
        String token = request.getParameter("newToken");
        if (token!=null){
            // 将token 放入cookie 中
            CookieUtil.setCookie(request,response,"token",token,WebConst.COOKIE_MAXAGE,false);
        }
        // 直接访问登录页面,当用户进入其他项目模块中。
        if (token==null){
            //  如果用户登录了,访问其他页面的时候不会有newToken,那么token 可能已经在cookie 中存在了
            token = CookieUtil.getCookieValue(request,"token",false);
        }
        // 已经登录的token,cookie 中的token。
        if (token!=null){
            // 去token 中的是有效数据,解密
            Map map = getUserMapByToken(token);
            String nickName = (String) map.get("nickName");
            request.setAttribute("nickName", nickName);
        }
        // Object handler
        // 获取方法,获取方法上的注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        LoginRequire methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequire.class);
        // 说明类上有注解!
        if (methodAnnotation!=null){
            //  必须要登录【调用认证】
            // 认证控制器在那个项目? 远程调用,
            String result = HttpClientUtil.doGet("http://sso.fg.cn:8080/sso/verify.do?token=" + token);
            if ("success".equals(result)){
                // 说明当前用户已经登录,保存userId : 购物车使用!
            	Map<String, Object> map = getUserinfoByToken(token);
                String username = (String) map.get("admin");
                request.setAttribute("username", username);
                return true;
            } else {
                // fail
                if (methodAnnotation.autoRedirect()){
                    // 认证失败!重新登录!
                    String requestURL  = request.getRequestURL().toString(); 
                    // 进行加密编码
                    String encodeURL = URLEncoder.encode(requestURL, "UTF-8");
                    response.sendRedirect("http://sso.fg.cn:8080/sso/index.do?redirectUrl=" + request.getRequestURL().toString());
                    return false;
                }
            }
        }
        return true;
    }

本案例cookie的设置是放在应用系统上,也可以考虑放在认证中心那边,少写点代码。但是要一定是在同一顶级域名下

package cn.fg.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

//Cookie工具类
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
                // 访问的域名 gmall.com
                cookie.setDomain(getDomainName(request));
            /**
             * WebContent
             * index.jsp
             * cookie.setDomain("localhost"); // 设置cookie的作用域
             * localhsot:8080/webTest/index.jsp 成功!
             * 127.0.0.1:8080/webTest/index.jsp 访问失败!
             *
             */
            // 当前根目录
                cookie.setPath("/");
            /**
             *  WebContent
             *  index.jsp
             *  index/index1.jsp
             *  cookie.setPath("/index");
             */

            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);
    }


}
发布了64 篇原创文章 · 获赞 0 · 访问量 3193

猜你喜欢

转载自blog.csdn.net/q42368773/article/details/103603959