JavaWeb简单的单点登录、验证码校验功能实现

时间:2021-12-31 17:09:48

前言

最近项目刚刚告一段落,后期有时间会慢慢分解整理出来给大家分享。本文主要提供思路和核心代码,建立在有一定后台基础读者上。(相信没有基础的同学只要认真细读也是可以理解的JavaWeb简单的单点登录、验证码校验功能实现

技术原理

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