基本流程
1 用户(输入用户名、密码),点击登录
login.html
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
2 前端进行md5加密,然后跳转到后端"/login/do_login"进行登录校验
<script>
function login(){
$("#loginForm").validate({
submitHandler:function(form){
doLogin();
}
});
}
function doLogin(){
g_showLoading();
var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
var password = md5(str);
$.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
3 在"login/do_login"中,有两件事,一是“@Valid LoginVo loginVo”可以接收手机号密码,同时通过jsr303做手机号格式校验,二是调用MiaoshauserService的login()方法。
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
userService.login(response, loginVo);
return Result.success(true);
}
4 先说接收手机号密码和手机号校验,它是通过jsr303做校验(面试只要说校验就好了,一开头十一个数字的正则表达式是"1\d{10}",然后校验非空,这不是面试重点)
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min=32)
private String password;
}
5 然后呢,在login()方法中,先通过输入的手机号判断用户是否存在,若不存在,则抛出"MOBILE_NOT_EXIST";若存在,则将前端传来的密码(已经过一次md5)和从数据库中取出的salt再次进行md5,将最终得到的值和数据库中的密码比较。如果二者不相等,则抛出“PASSWORD_ERROR”异常;若相等,则生成cookie,cookie中的值是一随机数token(每个用户不同),同时将此token作为redis的key,user作为redis的value保存在redis中。返回true,用户登录成功。
public boolean login(HttpServletResponse response, LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return true;
}
public static final String COOKI_NAME_TOKEN = "token";
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}
6 用户登录后,跳转到"/goods/to_list"
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";}
7 在to_list中,判断用户请求是否携带cookie,如果没有携带cookie,让用户重新登录(防止用户直接通过url访问商品列表页面);如果携带了cookie,则调用userService.getByToken方法,从redis中获取用户,然后,跳转到商品列表详情页 “goods_list.html”
@RequestMapping("to_list")
public String toList(@CookieValue(value= CookieUtil.COOKIE_NAME,required = false) String cookieToken,
@RequestParam(value = CookieUtil.COOKIE_NAME,required = false) String paramToken,
Model model,HttpServletResponse response){
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return "login";
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user = userService.getByToken(token,response);
model.addAttribute("user",user);
return "goods_list";
}
8 在userService.getByToken方法中,通过传入的token获取用户,同时需要重新设置redis的过期时间
public MiaoshaUser getByToken(String token,HttpServletResponse response) {
//先判断token是否为空
if(StringUtils.isEmpty(token)){
return null;
}
//根据token到redis中拿到相应的value
MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
redisService.set(MiaoshaUserKey.token,token,user);//key--->UserKey:tkUUID,value--->Serialized User
//如果此时拿到user成功了,这里要重新设置一下redis过期时间
if(user != null){
redisService.set(MiaoshaUserKey.token,token,user);
}
return user;
}
*代码改进(非必须)
我们发现,后面涉及到商品等其他的接口,按照这种写法,每次都要先获取cookie,然后从redis中获取user信息,获取成功,我们才能进行下一步操作。显然太过冗余,我们可以将其剥离出来,写在一个地方,避免冗余的代码。
我们的controller可以写成:
@RequestMapping("to_list")
public String toList(Model model,HttpServletResponse response,MiaoshaUser user){
model.addAttribute("user",user);
return "goods_list";
}
那么,我们在一个地方统一判断user是否能获取到。就要用到springmvc的机制了,我们可以试想springmvc支持的参数都是如何进来的呢?比如这里的MiaoShaUser是从什么地方注入进来的呢?
其实在UserArgumentResolver这个类中就可以拿到输入的参数,比如MiaoShaUser这个对象,然后再在resolveArgument这个方法里,对这个参数进行相应的处理:
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver{
@Autowired
private MiaoshaUserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz== MiaoshaUser.class;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(CookieUtil.COOKIE_NAME);
String cookieToken = CookieUtil.readLoginToken(request);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return "login";
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(token,response);
}
}
当然,这个对传入的参数进行修改的UserArgumentResolver要被重新加入进argumentResolvers中,相当于完成对原始的argumentResolvers中某个参数的重写:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
这样,只要某个方法中传入了MiaoShaUser这个对象,那么就会进入resolveArgument()这个方法进行判断是否能拿到这个对象(这个要注意!!!!)。
当然,我们可能更加常用的方式是springmvc拦截器来实现这个功能。并且在拦截器中,还可以实现更加复杂的逻辑,比如不仅可以判断user是否已经登陆,还可以针对特殊的url进行特别的处理。