链接:Kisso实例项目
版本:1.4
官方文档:kisso 帮助文档
Maven依赖项
/pom.xml
com.baomidou
mybatis-plus
1.2.12
com.baomidou
kisso
3.6.6
com.alibaba
fastjson
1.2.9
org.bouncycastle
bcprov-jdk15on
1.54
Spring MVC设置
/resource/spring-mvc.xml
Kisso设置
/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
使用自定义域名。
User映射设置
/mapper/userMapper.xml
insert into user(username,
password) values(#{username}, #{password})
登录时,根据username
,获取User
类。
加盐密码=MD5(用户名+原密码)
User映射接口
/mapper/UserMapper.java
public interface UserMapper {
/**
* 添加用户
* @param user 用户
* @return 修改的行数
*/
int addUser(User user);
/**
* 查询用户是否存在
* @param username 用户名
* @return
*/
int checkUserByUsername(String username);
/**
* 根据用户名返回用户信息
* @param user 用户名
* @return 用户信息
*/
List getUserInfoByName(User user);
/**
* 查询所有用户的信息
* @return 用户信息的表
*/
List findAllUser();
}
Java Bean
/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;
}
}
User服务接口
/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 findAllUser();
}
User服务实现
/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 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 findAllUser() {
return userMapper.findAllUser();
}
}
用户注册
/controller/UserController.java
@Login(action = Action.Skip)
@RequestMapping(value = "/reg", method = RequestMethod.POST)
public @ResponseBody Map addUser(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password) {
Map map = new HashMap();
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 login(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestParam(value = "verify") String verify) {
Map map = new HashMap();
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;
}
登录的逻辑:
-
@RequestParam
,规范参数格式 - 判断验证码,从
Session
中取 - 用
WafRequestWrapper
过滤SQL注入 - 将用户名和密码放入Java Bean,即
User
中 - 调用
UserService
的验证机制 - 查看结果
- 如果验证失败,则返回失败
- 如果成功,则新建
SSOToken
,放入userid
和username
- 用
SSOHelper.setSSOCookie(request, response, st, true)
完成SSO注册 - 最后,用户的
Cookie
中,有一项uid
是加密的,保存了用户的userid
和username
注意点:
- 为什么是
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();
}
}
注意点:
地址是
#{controller}/verify
,设为Action.Skip
,因为任何人都可以获取验证码,不写则默认为Action.Normal
启用认证。将验证码的明文存入
Session
中,待验证登录时取出。
绘图
引用自SpringWind。
来自CaptchaHelper.java中的/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 filters = new ArrayList();
//摆动干扰
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);
}
}
HTML
/WebContent/jsp/login.jsp
引入js / html
gup取参函数 / js
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];
}
}
初始化验证 / js
$(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个字符"
}
}
});
});
设置提交方式 / js
$.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("登录失败,请稍后再试。");
});
}
});
设置表单 / html
代码验证 / java
/controller/UserController.java
@Login(action = Action.Skip)
@RequestMapping(value = "/login", method = RequestMethod.POST)
public @ResponseBody Map login(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestParam(value = "verify") String verify) {
Map map = new HashMap();
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() {
/**
*
* SSO 退出,清空退出状态即可
*
*
*
* 子系统退出 SSOHelper.logout(request, response); 注意 sso.properties 包含 退出到
* SSO 的地址 , 属性 sso.logout.url 的配置
*
*/
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:/";
}
注意点:
- 使用
SSOHelper.clearLogin(request, response)
重定向
/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();
}
注意点:
- 返回200时,为成功,跳转
- 返回400时,为失败,刷新页面
登出的重定向
点击链接登出时,服务器返回302重定向。
HTML
JAVA
@RequestMapping(value = "/logout")
public String logout() {
// SSO清理工作
// ...
return "redirect:/";
}
注意点:
- 适用
ajax
。浏览器中的js跳转,地址可以从服务器写,如/ssm
- 适用
a href
。服务器的302、301跳转,Controller
方法返回String
,值为"redirect:path/to/redirect"
显示用户名
/WebContent/jsp/index.jsp
/WebContent/jsp/permission.jsp
HTML
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
${ userid },欢迎光临!
JAVA
SSOToken st = SSOHelper.getToken(request);
if (st != null) {
request.setAttribute("userid", st.getUid());
}
return "/index";
注意点:
- 模版引擎除了
JSTL
外还有Velocity
等。Velocity
充分体现了的MVC思想。
- 显示用户名的流程。
MVC简易流程:
- 控制层:利用Kisso获取用户信息,放入模型。
- 模型层:存放、传递数据。
- 视图层:根据模型,解析数据,渲染页面。
常见问题
ContextLoader类不存在
项目 -> 属性 -> Web Deployment Assembly
Add => Java Build Path Entries => Maven Dependencies
缺少类
修改pom.xml
,然后Update Project。
常用解决办法
- 清理Tomcat目录
- 重启Tomcat
- Classpath路径问题,增加JRE、Tomcat、Maven、Web App Lib
- Web Module问题,在项目属性中的Project Facets
- 修改容器名称,即localhost:8080/????,项目属性中的Web Project Settings
- Java文件错误,修改Java Compiler,即编译器版本
- 注意文件名和路径的大小写