https://www.jianshu.com/p/4026edd340bb
/pom.xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kisso</artifactId>
<version>3.6.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
/resource/spring-mvc.xml
<!-- kisso 注入初始化,也支持使用 web.xml 初始化 -->
<bean id="kissoInit" class="com.baomidou.kisso.web.WebKissoConfigurer" init-method="initKisso">
<property name="ssoPropPath" value="sso.properties" />
<!-- 测试模式 ,不同环境配置选择设置 -->
<property name="runMode" value="test_mode" />
<!-- 此处可以注入 SSOConfig 配置属性,也可以定义自己的 kisso 插件,基础 SSOPlugin 抽象类。
<property name="pluginList">
<list>
<bean name="com.xxxx.MyPlugin">
</list>
</property>
-->
</bean>
<mvc:interceptors>
<!-- SSO 拦截器 -->
<!-- path 对所有的请求拦截使用/**,对某个模块下的请求拦截使用:/myPath/* -->
<mvc:interceptor>
<mvc:mapping path="/user/*" />
<mvc:mapping path="/permission/*" />
<bean class="com.baomidou.kisso.web.interceptor.SSOSpringInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
/resource/sso.properties
################ SSOConfig file #################
sso.encoding=utf-8
sso.secretkey=30eb4892122c45fd0f
sso.cookie.name=uid
sso.cookie.domain=.vcap.me
sso.login.url=http://ssm.vcap.me:8080/ssm/user/tologin
或者
################ SSOConfig file #################
sso.encoding=utf-8
sso.secretkey=30eb4892122c45fd0f
sso.cookie.name=uid
sso.cookie.domain=127.0.0.1
sso.login.url=http://127.0.0.1:8080/ssm/user/tologin
domain不能为localhost,可修改hosts使用自定义域名。
/mapper/userMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mapper.UserMapper">
<!-- 解决表名与字段不匹配 -->
<resultMap type="User" id="userResultMap">
<result property="userid" column="userid" />
<result property="username" column="username" />
<result property="password" column="password" />
</resultMap>
<!-- 查询用户是否存在 -->
<select id="checkUserByUsername" resultType="int" parameterType="java.lang.String">
select count(1) from user WHERE username=#{username}
</select>
<!-- 添加用户 -->
<insert id="addUser" parameterType="User">
insert into user(username,
password) values(#{username}, #{password})
</insert>
<!-- 获取用户信息 -->
<select id="getUserInfoByName" resultType="User" parameterType="User">
select * from user WHERE username=#{username}
</select>
<!-- 查询所有用户-->
<select id="findAllUser" resultType="User">
select * from user
</select>
</mapper>
登录时,根据username,获取User类。
加盐密码=MD5(用户名+原密码)
/mapper/UserMapper.java
public interface UserMapper {
/**
* 添加用户
* @param user 用户
* @return 修改的行数
*/
int addUser(User user);
/**
* 查询用户是否存在
* @param username 用户名
* @return
*/
int checkUserByUsername(String username);
/**
* 根据用户名返回用户信息
* @param user 用户名
* @return 用户信息
*/
List<User> getUserInfoByName(User user);
/**
* 查询所有用户的信息
* @return 用户信息的表
*/
List<User> findAllUser();
}
/model/User.java
带有mybatis-plus.jar提供的注解,用于导出SQL语句。
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
public class User {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId
private Long userid;
private String username;
private String password;;
public User() {
super();
}
public Long getId() {
return userid;
}
public void setId(Long id) {
this.userid = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
/service/UserService.java
注意:Controller中带有Autowired注解的字段必须为接口。
public interface UserService {
/**
* 添加用户
* @param user 用户
* @return 修改的行数
*/
int addUser(User user);
/**
* 查询用户是否存在
* @param username 用户名
* @return
*/
boolean checkUserByUsername(String username);
/**
* 检查用户名和密码是否合法
* @param user 登录信息
* @return 成功则返回id,失败返回-1
*/
long validUserAndPassword(User user);
/**
* 查询所有用户的信息
* @return 用户信息的表
*/
List<User> findAllUser();
/service/impl/UserServiceImpl.java
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Resource
public UserMapper userMapper;
@Override
public int addUser(User user) {
int userid = userMapper.addUser(user);
return userid;
}
@Override
public boolean checkUserByUsername(String username) {
return userMapper.checkUserByUsername(username) == 1;
}
@Override
public long validUserAndPassword(User user) {
List<User> users = userMapper.getUserInfoByName(user);
if (users.isEmpty()) {
return -1;// 不存在
}
User info = users.get(0);
if (SaltEncoder.md5SaltValid(user.getUsername(), info.getPassword(), user.getPassword())) {
return info.getId();
} else {
return -1;// 不存在
}
}
@Override
public List<User> findAllUser() {
return userMapper.findAllUser();
}
}
/controller/UserController.java
@Login(action = Action.Skip)
@RequestMapping(value = "/reg", method = RequestMethod.POST)
public @ResponseBody Map<String, Object> addUser(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password) {
Map<String, Object> map = new HashMap<String, Object>();
if (userService.checkUserByUsername(username)) {
User user = new User();
user.setUsername(username);
user.setPassword(SaltEncoder.md5SaltEncode(username, password));
int id = userService.addUser(user);
logger.debug(String.format("add user: id=%d name=%s", id, username));
map.put("code", "200");
map.put("msg", "注册成功!");
} else {
logger.warn(String.format("conflict user: name=%s", username));
map.put("code", "400");
map.put("msg", "用户已存在!");
}
return map;
}
SaltEncoder.md5SaltEncode(登录名,原密码)=> 返回哈希密码
/controller/UserController.java
@Login(action = Action.Skip)
@RequestMapping(value = "/login", method = RequestMethod.POST)
public @ResponseBody Map<String, Object> login(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestParam(value = "verify") String verify) {
Map<String, Object> map = new HashMap<String, Object>();
String verifyCode = String.valueOf(request.getSession().getAttribute("verify"));
if (!verifyCode.equalsIgnoreCase(verify)) {
map.put("code", "400");
map.put("msg", "验证码错误");
return map;
}
request.getSession().removeAttribute("verify");
/**
* 生产环境需要过滤sql注入
*/
WafRequestWrapper req = new WafRequestWrapper(request);
String username_ = req.getParameter("username");
String password_ = req.getParameter("password");
User user = new User();
user.setUsername(username_);
user.setPassword(password_);
long userid = userService.validUserAndPassword(user);
if (userid != -1) {
logger.debug(String.format("login success: name=%s password=%s", username_, password_));
map.put("code", "200");
map.put("msg", "登录成功!");
/*
* authSSOCookie 设置 cookie 同时改变 jsessionId
*/
SSOToken st = new SSOToken(request);
st.setId(userid);
st.setUid(username_);
st.setType(1);
// 记住密码,设置 cookie 时长 1 天 = 86400 秒 【动态设置 maxAge 实现记住密码功能】
/*
* String rememberMe = req.getParameter("rememberMe"); if
* ("on".equals(rememberMe)) {
* request.setAttribute(SSOConfig.SSO_COOKIE_MAXAGE, 86400); }
*/
request.setAttribute(SSOConfig.SSO_COOKIE_MAXAGE, -1);//浏览器关闭自动删除cookie
SSOHelper.setSSOCookie(request, response, st, true);
} else {
logger.warn(String.format("wrong login: name=%s password=%s", username_, password_));
map.put("code", "400");
map.put("msg", "您输入的帐号或密码有误");
}
return map;
}
登录的逻辑:
注意点:
为什么是uid? sso.properties中的sso.cookie.name项
怎么加密?密钥在sso.properties中的sso.secretkey
/controller/UserController.java
/**
* 验证码 (注解跳过权限验证)
*/
@Login(action = Action.Skip)
@ResponseBody
@RequestMapping("/verify")
public void verify() {
try {
String verifyCode = CaptchaUtil.outputImage(response.getOutputStream());
request.getSession().setAttribute("verify", verifyCode);//把验证码存入session
logger.debug(String.format("verify code: %s", verifyCode));
} catch (IOException e) {
e.printStackTrace();
}
}
注意点:
/com/utils/CaptchaUtil.java
public class CaptchaUtil {
public static String outputImage(OutputStream out) throws IOException {
ConfigurableCaptchaService cs = new ConfigurableCaptchaService();
//验证码宽高
cs.setWidth(85);
cs.setHeight(35);
//设置 6 位自适应验证码
// AdaptiveRandomWordFactory arw = new AdaptiveRandomWordFactory();
// arw.setMinLength(6);
// arw.setMaxLength(6);
// cs.setWordFactory(arw);
//字符大小设置
RandomFontFactory rf = new RandomFontFactory();
rf.setMinSize(25);
rf.setMaxSize(28);
cs.setFontFactory(rf);
//文本渲染
// cs.setTextRenderer(new RandomYBestFitTextRenderer());
//设置一个单一颜色字体
cs.setColorFactory(new SingleColorFactory(new Color(59, 162, 9)));
// cs.setFilterFactory(new CurvesRippleFilterFactory(cs.getColorFactory()));
//图片滤镜设置
ConfigurableFilterFactory filterFactory = new ConfigurableFilterFactory();
List<BufferedImageOp> filters = new ArrayList<BufferedImageOp>();
//摆动干扰
WobbleImageOp wio = new WobbleImageOp();
wio.setEdgeMode(AbstractImageOp.EDGE_CLAMP);
wio.setxAmplitude(2.0);
wio.setyAmplitude(1.0);
filters.add(wio);
//曲线干扰
// CurvesImageOp cio = new CurvesImageOp();
// cio.setColorFactory(new SingleColorFactory(new Color(59, 162, 9)));
// cio.setEdgeMode(AbstractImageOp.EDGE_ZERO);
// cio.setStrokeMax(0.3f);
// cio.setStrokeMin(0.1f);
// filters.add(cio);
filterFactory.setFilters(filters);
cs.setFilterFactory(filterFactory);
//椭圆形干扰背景
// cs.setBackgroundFactory(new OvalNoiseBackgroundFactory(7));
//线形干扰背景
cs.setBackgroundFactory(new LineNoiseBackgroundFactory(37));
//输出验证图片
return EncoderHelper.getChallangeAndWriteImage(cs, "png", out);
}
}
/WebContent/jsp/login.jsp
<script src="${js_root}/js/jquery-1.11.1.js"></script>
<script src="${js_root}/js/jquery.validate.min.js"></script>
<script src="${js_root}/js/messages_zh.js"></script>
function gup(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(location.pathname);
if (results == null) {
return location.pathname;
} else {
return results[1];
}
}
$(document).ready(function() {
// validate the comment form when it is submitted
$("#signupForm").validate({
rules : {
username : {
required : true,
minlength : 2,
},
password : {
required : true,
minlength : 6
},
verify : {
required : true,
minlength : 4
}
},
messages : {
username : {
required : "请输入用户名",
minlength : "用户名至少由两个字符组成"
},
password : {
required : "请输入密码",
minlength : "密码长度不能小于 6 个字符"
},
verify : {
required : "请输入验证码",
minlength : "验证码长度为4个字符"
}
}
});
});
$.validator.setDefaults({
submitHandler : function() {
$.post(
// 接收数据的页面
'login',
// 传给后台的数据,多个参数用&连接或者使用json格式数据:{a:'value1',b:'value2'}
{
username : $("#username").val(),
password : $("#password").val(),
verify : $("#verify").val()
}, function(data) {
if (data.code == '200') {
alert("msg: " + data.msg + "\n" + "即将跳转。");
location.href = gup("ReturnURL");
} else if (data.code == '400') {
alert(data.msg);
location.reload();
}
},
// 默认返回字符串,设置值等于json则返回json数据
'json').error(function() {
alert("登录失败,请稍后再试。");
});
}
});
<form class="cmxform" id="signupForm" method="post" action="login">
<fieldset>
<legend>请输入你的用户名和密码</legend>
<p>
<label for="cusername">用户名</label> <input id="username"
name="username" type="text">
</p>
<p>
<label for="cpassword">密码</label> <input id="password"
name="password" type="password">
</p>
<p>
<label for="cverify">验证码</label> <input id="verify" name="verify"
type="text"> <img id="verifyImg"
onclick="javascript:this.src=('verify?reload='+(new Date()).getTime())"
src="verify" width="85" height="35" alt="点击查看验证码">
</p>
<p>
<input class="reset" type="reset" value="重置"> <input
class="submit" type="submit" value="登录">
</p>
</fieldset>
</form>
/controller/UserController.java
@Login(action = Action.Skip)
@RequestMapping(value = "/login", method = RequestMethod.POST)
public @ResponseBody Map<String, Object> login(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestParam(value = "verify") String verify) {
Map<String, Object> map = new HashMap<String, Object>();
String verifyCode = String.valueOf(request.getSession().getAttribute("verify"));
if (!verifyCode.equalsIgnoreCase(verify)) {
map.put("code", "400");
map.put("msg", "验证码错误");
return map;
}
request.getSession().removeAttribute("verify");
// 其他登录认证机制...
}
/controller/UserController.java
@RequestMapping(value = "/logout")
public String logout() {
/**
* <p>
* SSO 退出,清空退出状态即可
* </p>
*
* <p>
* 子系统退出 SSOHelper.logout(request, response); 注意 sso.properties 包含 退出到
* SSO 的地址 , 属性 sso.logout.url 的配置
* </p>
*/
SSOToken st = SSOHelper.getToken(request);
if (st != null) {
logger.debug(String.format("logout: id=%d, uid=%s", st.getId(), st.getUid()));
}
SSOHelper.clearLogin(request, response);
return "redirect:/";
}
/WebContent/jsp/login.jsp
function gup(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(location.pathname);
if (results == null) {
return location.pathname;
} else {
return results[1];
}
}
Kisso拦截器将未授权访问重定向至登录页,带ReturnURL参数,存放跳转前地址,登录成功后,自动跳回。
if (data.code == '200') {
alert("msg: " + data.msg + "\n" + "即将跳转。");
location.href = gup("ReturnURL");
} else if (data.code == '400') {
alert(data.msg);
location.reload();
}
注意点:
点击链接登出时,服务器返回302重定向。
<p><a href="tologout">登出</a></p>
@RequestMapping(value = "/logout")
public String logout() {
// SSO清理工作
// ...
return "redirect:/";
}
注意点:
/WebContent/jsp/index.jsp
/WebContent/jsp/permission.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- other -->
<p>${ userid },欢迎光临!</p>
SSOToken st = SSOHelper.getToken(request);
if (st != null) {
request.setAttribute("userid", st.getUid());
}
return "/index";
注意点:
MVC简易流程:
项目 -> 属性 -> Web Deployment Assembly
Add => Java Build Path Entries => Maven Dependencies
修改pom.xml,然后Update Project。