发表于: 2018-01-21 09:45:47
2 626
今天完成的事情:
1 梳理下注册和登录
注册时:
1首先注册者输入用户名、邮箱地址、密码等信息
2前端进行校验,检验所有输入信息的格式(例如邮件格式,密码强度),校验符合要求后
3发送ajax请求到后端,后端再次校验信息的格式
4还要校验唯一性字段是否存在,例如设计的是用户名是唯一的,就要校验用户名。
5校验成功,进入到注册用户的操作中:获取盐值,保存盐值及加密后的密码,保存用户。
6成功,跳转到登录界面。
第一次登录:
1 用户输入登录信息时,前端只校验是否为空,为空时,禁用登录按钮(按钮不起作用)
2 输入信息完成后,发送用户名、密码到后端进行校验。
3 匹配成功,设置cookie(长时间,几天左右),和session(短时间,十几分钟左右)
4 访问本域名下,其他网页时,先检查session,session过期再用cookie,cookie正确,重新创建session,cookie错误,提示信息:需要登录才能访问,并返回到登录页面。
5 回到第一步。
后续访问:
1未拦截的页面:不判断cookie和session状态,直接放行
2拦截的页面:进入拦截器prehandle方法,先判断session,再判断cookie,有一项通过就放行。
加密方法选择:
加密用户密码:随机字符串作为盐值,拼接盐值和明文密码,使用SHA256算法加密,保存盐值和加密后的密码
cookie value:使用jwt规范,HS256算法,暂时用固定秘钥,加密时,加密用户名,解密时用原来的固定秘钥解密。
其他方案:
用户密码:MD5加随机盐值,保存盐值和加密后的密码
cookie value(token):DES + 固定秘钥,加密信息:用户名+"."+登录时间,保存登录时间
使用考虑到MD5和DES作为加密来说,快过时了,就选用SHA256和jwt规范的HS256算法。
2 首先是user表
user_key用来放盐值,如果要放上次登录时间还要再加一个字段。
3 注册
3.1 controller层,处理逻辑:如果用户名存在,返回错误信息,如果用户名不存在,保存用户,返回正确信息
/**
* 注册 处理
* @param user
* @return
*/
@RequestMapping(value = "/signup", method = RequestMethod.POST)
@ResponseBody
public ResponseMsg saveUser(User user) {
if (userService.isUserNameExist(user.getUserName())) {
return ResponseMsg.fail();
} else {
userService.insertUser(user);
return ResponseMsg.success();
}
}
3.2 service提供:查询用户名是否存在(boolean),保存用户
task4\src\main\java\com\bpzj\task4\service\UserService.java
/**
* 检查 注册 用户名 是否存在
* @param userName 用户注册时 输入的用户名
* @return 用户名存在返回 true ,不存在返回 false
*/
public boolean isUserNameExist(String userName) {
// userNameExist
boolean userNameExist =false;
try {
// 如果原来就有用户名,不能注册,但是能登录
selectByUserName(userName);
userNameExist = true;
} catch (Exception e) {
userNameExist = false;
}
return userNameExist;
}
/**
* 根据 userName 从数据库 取得 User
* @param userName 登录者输入的用户名
* @return 如果有User,返回User,没有的话,抛出错误
* @throws Exception 不存在 对应 的 User
*/
public User selectByUserName(String userName) throws Exception{
UserExample example = new UserExample();
UserExample.Criteria criteria = example.createCriteria();
criteria.andUserNameEqualTo(userName);
List<User> userList = userMapper.selectByExample(example);
if (userList.isEmpty()) {
throw new Exception("用户名不存在");
} else {
return userList.get(0);
}
}
/**
* 保存用户到数据库,注册时调用
* @param user
* @return
*/
public int insertUser(User user) {
String salt = getSalt();
user.setUserKey(salt);
// 先拼接,再加密,再保存
user.setPassword(getSHA256Str(user.getPassword()+salt));
// 创建用户时间和更新时间
Date date =new Date();
Long time = date.getTime();
user.setCreateAt(time);
user.setUpdateAt(time);
return userMapper.insert(user);
}
3.3 使用到的加密工具类(获取盐值、和加密算法)
public class SHA256Util {
public static String getSHA256Str(String message){
MessageDigest messageDigest = null;
String SHAStr = "";
try {
// ============指定加密 算法============
messageDigest = MessageDigest.getInstance("SHA-256");
// ============更新加密信息到
messageDigest.update(message.getBytes("UTF-8"));
// ============得到bytes转换后的字符串
byte[] digest = messageDigest.digest();
SHAStr = bytesToHexString(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return SHAStr;
}
/**
* @return 返回 一个 盐值
*/
public static String getSalt(){
byte[] bytes = new byte[10];
Random ranGen = new SecureRandom();
ranGen.nextBytes(bytes);
// ( 用jdk math包的 BigInteger 把 bytes转换为16进制 )
// String str = new BigInteger(1,bytes).toString(16);
// 使用自己写的函数把 bytes转换为16进制,方法在下面
return bytesToHexString(bytes);
}
/**
* bytes 转为 16 进制字符串
* @param bytes
* @return
*/
private static String bytesToHexString(byte[] bytes) {
StringBuffer hexString = new StringBuffer();
// foreach 的用法
for (byte aByte:bytes) {
String hex = Integer.toHexString(0xff & aByte);
if (hex.length() == 1)
hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
/**
* 测试 加密方法
* @param args
*/
public static void main(String[] args) {
String salt = getSalt();
System.out.println(salt);
String message = "this is a message";
String encryptedMessage = getSHA256Str(message);
System.out.println(encryptedMessage);
}
}
4 登录
4.1 controller层,处理逻辑:如果checkLogin成功,就登录,反之失败
/**
* 登录 处理
* @param user
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResponseMsg login(HttpServletResponse response, HttpSession session, User user) {
if (userService.checkLogin(user, response,session)) {
return ResponseMsg.success();
} else {
return ResponseMsg.fail();
}
}
4.2 service层:checkLogin方法(boolean)
/**
* 检查 登录时 密码 是否正确
* @param user 登录用户传入的 数据,只有 用户名 和 密码
* @return 检验值,true 或 false
*/
public boolean checkLogin(User user, HttpServletResponse response, HttpSession session) {
boolean loginFlag = false;
User savedUser=null;
try {
// 从数据库中获取保存的用户
savedUser = selectByUserName(user.getUserName());
// 对比 加密后的 密码
loginFlag = savedUser.getPassword().equals(getSHA256Str(user.getPassword() + savedUser.getUserKey()));
} catch (Exception e) {
e.printStackTrace();
}
// 如果成功,设置 cookie, session
if (loginFlag) {
// 用JwtUtil生成token,秘钥为数据库中的 盐值,加密信息为 用户名
String token = JwtUtil.getJwtToken(savedUser.getUserName(),JWT_SECRET_KEY);
// 创建cookie,把 token 放到 cookie中,指定cookieName 为"key"
CookieUtil.creatCookie(response, COOKIE_NAME, token, 60 * 60 * 3);
// 设置session
session.setAttribute("userName",savedUser.getUserName());
// 这里时间单位 是 秒,设为10分钟
session.setMaxInactiveInterval(60*10);
}
return loginFlag;
}
4.3 用到的工具类jwt加密生成token、操作cookie的工具类
jwt 工具需要 jar包支持
public class JwtUtil {
/**
*
* @param subject 需要加密的信息
* @param signKey 加密用的秘钥
* @return 生成的 token 字符串
*/
public static String getJwtToken(String subject, String signKey) {
long nowMills = System.currentTimeMillis();
Date now = new Date(nowMills);
JwtBuilder jwtBuilder = Jwts.builder()
// subject 是需要加密的东西,这里是用户名
.setSubject(subject)
// 传入 当前时间,说明签发的时间
.setIssuedAt(now)
// 用 signKey 做秘钥,对信息加密
.signWith(SignatureAlgorithm.HS256, signKey);
return jwtBuilder.compact();
}
/**
* 解密 cookieValue值
* @param cookieToken 用户发送的,加密后的 cookie值
* @param signKey 解密时用的 秘钥
* @return 解密后的信息
*/
public static String jwtDecrypt(String cookieToken, String signKey) {
String message="";
try{
message= Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(cookieToken)
.getBody()
.getSubject();
} catch (Exception e){
// 如果错误,直接把message设置为空字符串。
message = "";
}
return message;
}
}
public class CookieUtil {
public static void creatCookie(
HttpServletResponse response,
String cookieName,
String cookieValue,
int cookieMaxAge) {
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(cookieMaxAge);
// 指定uri
cookie.setPath("/");
response.addCookie(cookie);
}
public static void clearCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
/**
* 根据 cookie 的名称,获取 cookie 的值
* @param request 请求端发送的请求,应该包括 cookie
* @param cookieName 指定的cookie名称
* @return cookie 的值
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
// 使用了webUtil这个类,里面集成了getCookie的方法
Cookie cookie = WebUtils.getCookie(request, cookieName);
if (cookie == null) {
return null;
}
return cookie.getValue();
}
}
5 拦截器
5.1 先注册拦截器
在springMVC的配置文件中
<mvc:interceptors>
<mvc:interceptor>
<!-- 对 /u/** 的请求进行拦截 -->
<mvc:mapping path="/u/**"/>
<bean class="com.bpzj.task4.aop.UserInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
拦截器的preHandle方法返回false的话,就不再执行对应的controller方法了。
所以要在返回false之前,跳转页面,或者返回错误信息给用户。
5.2 整理逻辑:
先看session,再看cookie,cookie 的 value需要解密后,和数据库中数据对比
返回false之前,可以直接跳转,也可以返回错误信息,这里返回了一个json字符串
public class UserInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// ===================优先用 session===================
HttpSession session = request.getSession();
String userName = (String) session.getAttribute("userName");
// 已登录
if (null != userName) {
return true;
}
// ===================再判断 cookie===================
// 从用户请求获取 cookie的值,这里是用户名经过 jwt加密后的 token 值
String cookieValue = CookieUtil.getCookieValue(request, COOKIE_NAME);
if (cookieValue == null) {
returnFail(response);
return false;
}
// 从 cookie 值中 解密得 用户名
String decryptedName = JwtUtil.jwtDecrypt(cookieValue, JWT_SECRET_KEY);
// 如果用户名为空
if (null == decryptedName) {
returnFail(response);
//request.getRequestDispatcher("/login").forward(request, response);
return false;
}
// 如果 用户名 在 数据库 中 存在
if (userService.isUserNameExist(decryptedName)) {
// 将 用户名 加到session中,不退出浏览器时就只需判断session即可
session.setAttribute("userName", decryptedName);
return true;
}
returnFail(response);
//request.getRequestDispatcher("/login").forward(request, response);
return false;
}
private void returnFail(HttpServletResponse response) throws IOException{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 输出流
// PrintWriter是一种过滤流,也叫处理流。也就是能对字节流和字符流进行处理
PrintWriter out =response.getWriter();
out.append("{\"code\":\"600\"}");
}
@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
5.3 用到的工具类:
根据cookie name获得cookie value,解密cookie 的 value值,用户名是否存在
见上面
6 注销
6.1 清除cookie,清除session
@RequestMapping(value = "/signout")
public String signOut(HttpServletResponse response, HttpSession session) {
// 清除cookie
CookieUtil.clearCookie(response,COOKIE_NAME);
// 清除session
session.invalidate();
return "/index";
}
跳转到首页
6.2 用到的工具类
清除cookie
见上
7 一个单独的controller
检验用户名是否已经存在
/**
* 检验用户名,是否和数据库中的重复
* @param userName 用户输入的 userName
* @return 自定义的格式,使用 Jackson 转为 json 字符
*/
@RequestMapping(value = "/checkName", method = RequestMethod.POST)
@ResponseBody
public ResponseMsg checkUserName(@RequestParam("userName")String userName) {
if (userService.isUserNameExist(userName)) {
return ResponseMsg.fail();
} else {
return ResponseMsg.success();
}
}
在用户名输入框中,如果文本发生变化,直接发送ajax请求,判断更改后的用户名是否存在。
js代码:
// 注册时,文本内容改变的话,开始校验用户名
$("#sign_name_input").change(function () {
// 提交数据先进行前端校验
if(!form_validate_name("#sign_name_input")) {
return false;
}
// 需要校验的 用户名,就是输入框的value值
var userName = this.value;
$.ajax({
url:"${APP_PATH}/checkName",
type:"POST",
data:"userName="+userName,
// result 是controller函数返回对象(序列化后的json字符)
success:function (result) {
if(result.code==200){
// 使用抽取的函数 显示提示信息
show_validate_msg("#sign_name_input","error","用户名已存在");
// 点击注册按钮,没有效果,添加一个自定义属性
$("#sign_up_modal_sign_btn").attr("ajax-value","error");
}else{
show_validate_msg("#sign_name_input","success","用户名可用");
$("#sign_up_modal_sign_btn").attr("ajax-value","success");
}
}
});
})
8 前端校验:
两次密码是否输入一致。
暂时没做
9 深度思考
1.什么是session?什么是cookie?session和cookie有什么区别?什么场景适用于session?什么场景适用于cookie?
两个都是用来保存用户状态的,为了解决http协议是无状态协议被创造出来的。
session是在服务器,存放了一堆数据,可以是明文数据,在springmvc中,数据放在sessionScope中,客户端那里放了一个session 的 id值。
客户端:
服务器:真正的数据存放在attributes中,添加数据用setAttribute方法
用户请求时,会发送这个session id值,服务器得到后,就可以验证了。
注意,这里的session id值也是放在了cookie中,见下图,如果用户禁用了cookie,这是session id就无法放到cookie中,这时可以使用uri重定向,把session id,拼接到uri中,传给服务器,保持session的正常使用。
cookie是把加密后的数据放在了客户端
服务器拿到后使用约定的秘钥、算法解密,就可以拿到解密后的值,然后。。。。
为什么要拼接一个登录时间:保证每次加密后生成的 cookie value(token) 不一致,更不易被破解
cookie在客户端是明文,所以要加密
适用场景:
cookie放在客户端,服务器压力较小,session相反。
用户过多时,适用session压力较大
2.拦截器、过滤器、监听器各有什么作用?
晚上再查,先开始任务6
明天计划的事情:
收尾任务5,进行任务6
遇到的问题:
一个脑残的问题:
注册功能,保存用户时,总是保存两次。原因竟然是insert语句写了两次,关键是查了半个小时,太不注意了。
收获:
见上。
forEach的用法
改为for each形式
评论