登录功能
Controller层实现如下,其中,Const是一个新建的状态类,存储各类常量值,包括当前登录用户的字符串名称,并将它会作为键值存储在Session域中。
@RequestMapping(value = "login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> login(String username, String password, HttpSession session){
ServerResponse<User> response = iUserService.login(username, password);
if(response.isSuccess()){
session.setAttribute(Const.CURRENT_USER,response.getData());
}
return response;
}
这是Service层的实现代码,首先接收到Controller层传递过来的用户名和密码,在数据库中校验用户名,然后对密码进行MD5加密,将加密后的数据拿到数据库中校验,校验通过,返回给前端用户名和成功消息。
@Autowired
private UserMapper userMapper;
@Override
public ServerResponse<User> login(String username, String password) {
int count = userMapper.checkUsername(username);
if(count<=0){
return ServerResponse.createByErrorMessage("用户名不存在");
}
//MD5密码验证
String Md5Password = MD5Util.MD5EncodeUtf8(password);
User user = userMapper.selectLogin(username, Md5Password);
if(user==null){
return ServerResponse.createByErrorMessage("密码错误");
}
user.setPassword(StringUtils.EMPTY);
return ServerResponse.createBySuccess("登录成功",user);
}
MD5加密算法如下,其中加入了盐值增加了破解难度:
/**
* 返回大写MD5
*
* @param origin
* @param charsetname
* @return
*/
private static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString.toUpperCase();
}
public static String MD5EncodeUtf8(String origin) {
origin = origin + PropertiesUtil.getProperty("password.salt", ""); //加上盐值 增加破解难度
return MD5Encode(origin, "utf-8");
}
登出、注册、校验功能
注册功能用到一个新定义的Const类,这是一个通用状态类,使用了静态常量、枚举、接口三种定义方式,以后会常常用到,注册需要填写用户名和邮箱,两种都要判断是否已存在的问题,同时对密码加密处理。
public ServerResponse<String> register(User user){
ServerResponse<String> validResponse = checkValid(user.getUsername(), Const.USERNAME);
if(!validResponse.isSuccess()){
return validResponse;
}
validResponse = checkValid(user.getEmail(), Const.EMAIL);
if(!validResponse.isSuccess()){
return validResponse;
}
user.setRole(Const.Role.ROLE_CUSTOMER);
//MD5加密
user.setPassword(MD5Util.MD5EncodeUtf8(user.getPassword()));
int resultCount = userMapper.insert(user);
if(resultCount==0){
return ServerResponse.createByErrorMessage("注册失败");
}
return ServerResponse.createBySuccessMessage("注册成功");
}
/**
* 校验是否存在,供注册方法复用
* @param str
* @param type
* @return
*/
public ServerResponse<String> checkValid(String str,String type){
if(!StringUtils.isBlank(type)){
int resultCount=0;
if(Const.USERNAME.equals(type)){
resultCount =userMapper.checkUsername(str);
if(resultCount>0){
return ServerResponse.createByErrorMessage("用户名已存在");
}
}
if(Const.EMAIL.equals(type)){
resultCount = userMapper.checkEmail(str);
if(resultCount>0){
return ServerResponse.createByErrorMessage("email已存在");
}
}
}else{
return ServerResponse.createByErrorMessage("参数错误");
}
return ServerResponse.createBySuccess();
}
重置密码功能
重置密码功能与密保问题的设置相关,首先是选择问题,从当前登录用户中选择注册时设置的问题,用Google瓜娃StringUtils判断是否为空。
public ServerResponse selectQuestion(String username){
ServerResponse<String> response = checkValid(username, Const.USERNAME);
if(response.isSuccess()){
return ServerResponse.createByErrorMessage("用户名不存在");
}
String question=userMapper.selectQuestionByUsername(username);
if(StringUtils.isNotBlank(question)){
return ServerResponse.createBySuccessMessage(question);
}else{
return ServerResponse.createByErrorMessage("找回密码的问题是空的");
}
}
验证答案是否正确,这里用到了加token的做法,主要是为了防止产生横向越权的问题,token使用UUID(全球统一标识码),存储在瓜娃提供的缓存中,并且设定过期时间(12小时)
//LRU(Least recently used,最近最少使用)缓存淘汰算法
private static LoadingCache<String,String> localCache= CacheBuilder.newBuilder().initialCapacity(1000).maximumSize(10000).expireAfterAccess(12, TimeUnit.HOURS)
.build(new CacheLoader<String, String>() {
//默认的数据加载实现 当调用get取值的时候 如果Key没有对应的值 就调用这个方法进行加载
@Override
public String load(String key) throws Exception {
return "null";
}
});
要过期时间的原因是,这次修改需要保证在一段时间内有效,过期就无效了。因为token也可以被拦截~~ 再进一步的做法是使用token修改完之后,把token置成失效。结合“忘记密码重置密码”(即forget_reset_password.do)这个接口一起看,例如不加token,那么通过修改密码的接口 就可以随便改其他username的密码了。如果加token,加了有效时间,起码在一段时间内,我保证自己的修改是有效且防止其他无效。
这在互联网上修改密码是一个很常用的做法,例如,忘记密码修改邮件里面给的链接,都会有一个提示,告诉你,这个修改密码的链接在10个小时之内有效,过期请重新获取该链接~
public ServerResponse<String> checkAnswer(String username,String question,String answer){
int resultCount = userMapper.checkAnswer(username, question, answer);
if(resultCount>0){
String forgetToken = UUID.randomUUID().toString();
TokenCache.setKey(Const.TOKEN_PREFIX+username,forgetToken);
return ServerResponse.createBySuccess(forgetToken);
}
return ServerResponse.createByErrorMessage("答案错误");
}
//必须答对密保问题才可以重置密码
public ServerResponse<String> forgetResetPassword(String username,String passwordNew,
String forgetToken){
if(StringUtils.isBlank(forgetToken)){
return ServerResponse.createByErrorMessage("参数错误,token需要传递");
}
ServerResponse<String> response = checkValid(username, Const.USERNAME);
if(response.isSuccess()){
return ServerResponse.createByErrorMessage("用户名不存在");
}
String token = TokenCache.getKey(Const.TOKEN_PREFIX + username);
if(StringUtils.isBlank(token)){
return ServerResponse.createByErrorMessage("token无效或过期");
}
if(StringUtils.equals(forgetToken,token)){
String Md5Password = MD5Util.MD5EncodeUtf8(passwordNew);
Integer resultCount = userMapper.updatePasswordByUsername(Md5Password,username);
if(resultCount!=null&&resultCount>0){
return ServerResponse.createBySuccessMessage("密码重置成功");
}else{
return ServerResponse.createByErrorMessage("修改密码失败");
}
}else{
return ServerResponse.createByErrorMessage("token错误,请重新获取重置密码的token");
}
}
//防止横向越权 一定要验证旧密码 确定是指定用户 也要附加Id进行密码查询 因为密码有重复 count(1)结果可能大于1 结果就是true了
// 第一步验证用户身份,这时提交第一个请求报文,验证成功之后,进入第二步;
// 第二步才是真正的修改密码的动作,而修改密码的POST数据包有3个请求参数,分别是新密码、确认新密码以及账号值。
// 问题就出在第二步,在执行修改密码的动作时,服务器并未验证被修改密码的账户是否是第一步中通过身份验证的账户,
// 因此攻击者可以很容易的以自己的身份通过认证,然后修改第二步提交的报文,实现对任意账户的密码修改!
public ServerResponse<String> resetPassword(String passwordOld,String passwordNew,User user) {
int resultCount = userMapper.checkPassword(MD5Util.MD5EncodeUtf8(passwordOld), user.getId());
if(resultCount==0){
return ServerResponse.createByErrorMessage("旧密码错误");
}
user.setPassword(MD5Util.MD5EncodeUtf8(passwordNew));
resultCount = userMapper.updateByPrimaryKeySelective(user);
if(resultCount>0){
return ServerResponse.createBySuccessMessage("密码更新成功");
}
return ServerResponse.createByErrorMessage("密码更新失败");
}
更新个人信息
同样也必须注意越权问题,用户名和email同时检查.
public ServerResponse<String> updateInfomation(User user){
//username是不能被更新的
//email也要进行一个校验,校验新的email是不是已经存在,并且存在的email如果相同的话,不能是我们当前的这个用户的.
int resultCount = userMapper.checkEmailByUserId(user.getEmail(), user.getId());
if(resultCount>0){
return ServerResponse.createByErrorMessage("用户邮箱已存在");
}
User update_user=new User();
update_user.setPhone(user.getPhone());
update_user.setQuestion(user.getQuestion());
update_user.setAnswer(user.getAnswer());
update_user.setEmail(user.getEmail());
update_user.setId(user.getId());
int countResult = userMapper.updateByPrimaryKeySelective(update_user);
if(countResult>0){
return ServerResponse.createBySuccessMessage("更新个人信息成功");
}
return ServerResponse.createByErrorMessage("修改失败");
}
获取用户详细信息
在Contoller实现如下:
Const账户中取出当前用户即可。
@RequestMapping(value="getUserInfo.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> getUserInfo(HttpSession session){
User user = (User) session.getAttribute(Const.CURRENT_USER);
if(user!=null){
return ServerResponse.createBySuccess(user);
}else{
return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户信息");
}
}