发表于: 2018-01-21 09:45:47

2 627


今天完成的事情:

1  梳理下注册和登录

注册时:

1首先注册者输入用户名、邮箱地址、密码等信息

2前端进行校验,检验所有输入信息的格式(例如邮件格式,密码强度),校验符合要求后

3发送ajax请求到后端后端再次校验信息的格式

4还要校验唯一性字段是否存在,例如设计的是用户名是唯一的,就要校验用户名。

5校验成功,进入到注册用户的操作中:获取盐值,保存盐值及加密后的密码,保存用户。

6成功,跳转到登录界面。

 

第一次登录:

1 用户输入登录信息时,前端只校验是否为空,为空时,禁用登录按钮(按钮不起作用)

2 输入信息完成后,发送用户名、密码到后端进行校验。

3 匹配成功,设置cookie(长时间,几天左右),和session(短时间,十几分钟左右)

4 访问本域名下,其他网页时,先检查sessionsession过期再用cookiecookie正确,重新创建sessioncookie错误,提示信息:需要登录才能访问,并返回到登录页面。

5 回到第一步。

 

后续访问:

1未拦截的页面:不判断cookiesession状态,直接放行

2拦截的页面:进入拦截器prehandle方法,先判断session,再判断cookie,有一项通过就放行。

 

加密方法选择:

加密用户密码:随机字符串作为盐值,拼接盐值和明文密码,使用SHA256算法加密,保存盐值和加密后的密码

cookie value:使用jwt规范,HS256算法,暂时用固定秘钥,加密时,加密用户名,解密时用原来的固定秘钥解密。

 

其他方案:

用户密码:MD5加随机盐值,保存盐值和加密后的密码

cookie value(token)DES + 固定秘钥,加密信息:用户名+"."+登录时间,保存登录时间

 

使用考虑到MD5DES作为加密来说,快过时了,就选用SHA256jwt规范的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,再看cookiecookie 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?什么是cookiesessioncookie有什么区别?什么场景适用于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形式


返回列表 返回列表
评论

    分享到