前言
最近项目刚刚告一段落,后期有时间会慢慢分解整理出来给大家分享。本文主要提供思路和核心代码,建立在有一定后台基础读者上。(相信没有基础的同学只要认真细读也是可以理解的)
技术原理
1、单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
现实中举个栗子:颐和园是北京著名的旅游景点。在颐和园内部有许多独立的景点,例如“苏州街”、“佛香阁”和“德和园”,都可以在各个景点门口单独买票。很多游客需要游玩所有的景点,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿进拿出的,容易丢失,很不安全。于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门 口出示一下刚才买的套票就能够被允许进入每个独立的景点。
单点登录也一样,当用户第一次访问应用系统的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份效验(eg:用户名、密码、验证码校验),如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问应用其他模块就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验(页面拦截器校验),检查ticket的合法性。如果通过效验,用户就可以在不用再次登录的情况下访问。
笔者代码实现机制:建立用户表SYS_USER存放用户名、密码、用户id等字段,用到的唯一认证凭据ticket指的是:用户名(loginName)、用户id(userId),用户校验登录成功后,用session存储凭据,当用户切换界面时,通过拦截器LoginInterceptor校验用户是否带有认证凭据,从而实现单点登录。
2、验证码校验:加载登录首页时,通过Get方式获取后台生成的校验码,同时后台用session存储验证码(为后续检验做准备),当前台检测到用户填写完验证码时,触发机制,通过Get方式传参给后台匹配实现检验机制。
逻辑代码
1、单点登录
控制层LoginCtroller.java(result_code为0表示登录校验成功,session保存的就是用户认证凭据)
@RequestMapping(value ="login.do",method = {RequestMethod.POST}) public @ResponseBody JSONObject login(@RequestBody JSONObject loginJson,HttpServletRequest request, HttpServletResponse response){ // 登录校验 JSONObject resultJson = userService.login(loginJson); if (resultJson.getIntValue("result_code") == 0) { SysUser sysUser =(SysUser) resultJson.get("sysUser"); // 创建登录Session信息 resultJson.put("id", sysUser.getId()); resultJson.put("name", sysUser.getName()); resultJson.put("loginName", sysUser.getLoginName()); this.initSession(request, sysUser); logger.info(String.format("用户:%s 登录系统,登录时间:%s", loginJson.getString("loginName"))); } return resultJson; } private void initSession(HttpServletRequest request,SysUser sysUser) { //创建登录Session信息 HttpSession httpSession = request.getSession(); httpSession.setAttribute("loginName", sysUser.getLoginName()); httpSession.setAttribute("userId", sysUser.getId()); }接口实现类UserServiceImpl.java(接口类UserService.java)中的登录校验方法,这里面主要是获取前台传递的用户信息参数,再通过用户名查询数据库用户信息,可能难点是用MD5密码加密核对信息进行校验。
public JSONObject login(JSONObject jSONObject){ JSONObject resultJson = new JSONObject(); try { if(StringUtils.isBlank( jSONObject.getString("loginName"))){ throw new RuntimeException("登录用户名不能为空!"); } if(StringUtils.isBlank( jSONObject.getString("password"))){ throw new RuntimeException("登录必须填写密码!"); } String loginName = jSONObject.getString("loginName"); SysUser sysUser = sysUserMapper.findByLoginName(loginName); if(sysUser==null){ throw new RuntimeException("用户不存在!"); } if(StringUtils.isBlank(jSONObject.getString("password"))){ throw new RuntimeException("登录密码不能为空!"); } String password = MD5Util.getMD5String(jSONObject.getString("password")); if(StringUtils.isBlank(sysUser.getPassword()) || !sysUser.getPassword().equals((password))){ throw new RuntimeException("密码错误!"); } resultJson.put("result_code", 0); resultJson.put("result_detail", "success"); resultJson.put("sysUser", sysUser); } catch (RuntimeException e){ resultJson.put("result_code", -2); resultJson.put("result_detail", e.getMessage()); logger.error("login ",e); }catch (Exception e){ resultJson.put("result_code", -1); resultJson.put("result_detail", e.getMessage()); logger.error("login ",e); } return resultJson; }加密工具类MD5Util.java
package com.kilomob.powernetwork.permission.common; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author fengjk * */ public class MD5Util { private static Log log = LogFactory.getLog(MD5Util.class); /** * 默认的密码字符串组合,apache校验下载的文件的正确性用的就是默认的这个组合 */ protected static char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; protected static MessageDigest messagedigest = null; static { try { messagedigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException nsaex) { log.error(MD5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。"); nsaex.printStackTrace(); } } public static String getMD5String(String str){ if(str!=null){ messagedigest.update(str.getBytes()); return bufferToHex(messagedigest.digest()); }else{ return null; } } private static String bufferToHex(byte bytes[]) { return bufferToHex(bytes, 0, bytes.length); } private static String bufferToHex(byte bytes[], int m, int n) { StringBuffer stringbuffer = new StringBuffer(2 * n); int k = m + n; for (int l = m; l < k; l++) { appendHexPair(bytes[l], stringbuffer); } return stringbuffer.toString(); } private static void appendHexPair(byte bt, StringBuffer stringbuffer) { char c0 = hexDigits[(bt & 0xf0) >> 4]; char c1 = hexDigits[bt & 0xf]; stringbuffer.append(c0); stringbuffer.append(c1); } }拦截器配置applicationContext.xml
<!-- 类的存放路径class ignoreUrlList和interceptro指的是忽略,不拦截--> <bean id="loginInterceptor" class="com.kilomob.powernetwork.managerweb.interceptor.LoginInterceptor"> <property name="loginPage" value="/login.html"></property> <property name="ignoreUrlList"> <list> <value>/api/login.do</value> <value>/login.html</value> <value>/api/loginout.do</value> <value>/api/loginValidate.do</value> <value>/api/imgcode</value> <value>/api/vali/imagecode</value> </list> </property> </bean> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/api/imgcode"/> <mvc:exclude-mapping path="/api/vali/imagecode"/> <ref bean="loginInterceptor"/> </mvc:interceptor> </mvc:interceptors>注意此处的loginPage和ignoreUrlList应与下面的拦截类变量名一致。
拦截类LoginInterceptor.java
package com.kilomob.powernetwork.managerweb.interceptor;
import java.io.File;
import java.util.List;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.kilomob.powernetwork.managerweb.util.WebConfig;
/**
* @Description:登录拦截器
* @author: fengjk
* @time:2017年3月20日 下午8:11:25
*/
public class LoginInterceptor implements HandlerInterceptor {
private String loginPage;
private List<String> ignoreUrlList;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public List<String> getIgnoreUrlList() {
return ignoreUrlList;
}
public void setIgnoreUrlList(List<String> ignoreUrlList) {
this.ignoreUrlList = ignoreUrlList;
}
@Override
public boolean preHandle(HttpServletRequest paramHttpServletRequest,HttpServletResponse paramHttpServletResponse, Object paramObject)throws Exception {
paramHttpServletResponse.addHeader("P3P", "CP=CAO PSA OUR");
String path = paramHttpServletRequest.getRequestURI();
boolean ignore = false;
for (String url : ignoreUrlList) {
if (path.contains(url)) {
ignore = true;
break;
}
}
if (ignore) {
return true;
}
HttpSession httpSession = paramHttpServletRequest.getSession();
if (httpSession.getAttribute("userId") == null && httpSession.getAttribute("loginName") == null) {
paramHttpServletResponse.setContentType("text/html;charset=UTF-8");
paramHttpServletResponse.sendRedirect("http://127.0.0.1:8080/managerweb/login.html");
return false;
}
paramHttpServletRequest.setAttribute("loginName", httpSession.getAttribute("loginName"));
paramHttpServletRequest.setAttribute("userId", httpSession.getAttribute("userId"));
return true;
}
@Override
public void postHandle(HttpServletRequest paramHttpServletRequest,HttpServletResponse paramHttpServletResponse, Object paramObject,ModelAndView paramModelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest paramHttpServletRequest,HttpServletResponse paramHttpServletResponse, Object paramObject,Exception paramException) throws Exception {
}
}
通过session检验用户是否已经登录过,否的话则跳转回首页。关于CAS实现的单点登录可参考:http://blog.csdn.net/small_love/article/details/6664831/
2、验证码校验
控制层LoginCtroller.java
@RequestMapping(value ="/imgcode",method = {RequestMethod.GET}) public void getImgCode(HttpServletRequest request,HttpServletResponse response) throws IOException { HttpSession session = request.getSession(); session.removeAttribute("code"); response.setContentType("image/jpeg"); ServletOutputStream sos = response.getOutputStream(); response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); BufferedImage image = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); char[] rands = generateCheckCode(); drawBackground(g); drawRands(g, rands); g.dispose(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ImageIO.write(image, "JPEG", bos); byte[] buf = bos.toByteArray(); response.setContentLength(buf.length); sos.write(buf); bos.close(); sos.close(); session.setAttribute("code", new String(rands)); } private void drawBackground(Graphics g) { g.setColor(new Color(72, 75, 83)); g.fillRect(0, 0, WIDTH, HEIGHT); /*for (int i = 0; i < 120; i++) { int x = (int) (Math.random() * WIDTH); int y = (int) (Math.random() * HEIGHT); int red = (int) (Math.random() * 255); int green = (int) (Math.random() * 255); int blue = (int) (Math.random() * 255); g.setColor(new Color(red, green, blue)); g.drawOval(x, y, 1, 0); }*/ } private void drawRands(Graphics g, char[] rands) { g.setColor(new Color(0xe0e0e0)); g.setFont(new Font("Arial", Font.BOLD | Font.ITALIC, 24)); g.drawString("" + rands[0], 1, 27); g.drawString("" + rands[1], 19, 25); g.drawString("" + rands[2], 39, 27); g.drawString("" + rands[3], 58, 26); } private char[] generateCheckCode() { String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; char[] rands = new char[4]; for (int i = 0; i < 4; i++) { int rand = (int) (Math.random() * 62); rands[i] = chars.charAt(rand); } return rands; } /** * @Description:校验验证码 * @param imagecode * @param request * @param response * @return * boolean * @exception: * @author: fengjk * @time:2017年3月27日 下午3:45:12 */ @RequestMapping(value= "/vali/imagecode/{imagecode}" ,method = {RequestMethod.GET} ) public int valideImage(@PathVariable(name = "imagecode") String imagecode,HttpServletRequest request,HttpServletResponse response) { HttpSession session = request.getSession(); String code = (String)session.getAttribute("code"); if(code != null && code.toUpperCase().equals(imagecode.toUpperCase())){ return 0; } return 1; }前台加载首页时通过Get方式请求getImgCode方法获取验证码,后台同时用session保存数据,当校验验证码时,通过valideImage方法校验,返回0说明校验成功。
总结
篇幅不长,相信读者在理解实现原理基础上回归代码会比较通俗易懂。文章如有误处和不足,请及时留言告知笔者,万分感谢!欢迎加群互相探讨学习,qq:583138104