秒杀项目02-实现登录功能

本文详细介绍了如何使用MD5加盐技术增强密码安全性,包括客户端和服务器端的加密流程,以及利用JSR303参数校验和全局异常处理器进行验证。此外,还展示了如何通过Redis实现分布式Session,生成和管理用户登录状态的token。
摘要由CSDN通过智能技术生成

1. 数据库设计

create table miaosha_user (
    id bigint(20) not null comment '用户ID, 手机号码',
    nickname varchar(255) not null,
    password varchar(32) default null comment 'MD5(MD5(pass明文+固定salt) + salt)',
    salt varchar(10) default null,
    head varchar(128) default null comment '头像,云存储的ID',
    register_date datetime default null comment '注册时间',
    last_login_date datetime default null comment '上次登录时间',
    login_count int(11) default '0' comment '登录次数',
    primary key (id)
)engine=innoDB charset=utf8mb4

2. 明文密码两次MD5处理

导入依赖

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
</dependency>

编写MD5Utils

public class MD5Util {

    public static String md5(String src) {
        return DigestUtils.md5Hex(src);
    }

    private static final String salt = "hmxP@ssw0rd";

    /**
     * 用户输入的密码经过固定的salt MD5首次加密
     * @param inputPass
     * @return
     */
    public static String inputPassToFormPass(String inputPass) {
        String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }

    /**
     * 加密后的密码再次经过可变的salt MD5加密后存到数据库中
     * @param fromPass
     * @param salt
     * @return
     */
    public static String formPassToDBPass(String fromPass, String salt) {
        String str = "" + salt.charAt(0) + salt.charAt(2) + fromPass + salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }

    /**
     * 双重MD5加密
     * @param input
     * @param saltDB
     * @return
     */
    public static String inputPassToDBPass(String input, String saltDB) {
        return formPassToDBPass(inputPassToFormPass(input),saltDB);
    }

    public static void main(String[] args) {
        System.out.println(inputPassToFormPass("hmx"));
        System.out.println(formPassToDBPass("hmx", "hmxsfd"));
        System.out.println(inputPassToDBPass("hmx","12343sf"));
    }

}

2.1. 用户端: PASS = MD5(明文 + 固定Salt)

common.js

//展示loading
function g_showLoading(){
	var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;  
	return idx;
}
//salt
var g_passsword_salt="hmxP@ssw0rd"

login.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
    <!-- jquery-validator -->
    <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
    <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- md5.js -->
    <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
    <!-- common.js -->
    <script type="text/javascript" th:src="@{/js/common.js}"></script>

</head>
<body>

<form name="loginForm" id="loginForm" method="post"  style="width:50%; margin:0 auto">

    <h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>

    <div class="form-group">
        <div class="row">
            <label class="form-label col-md-4">请输入手机号码: </label>
            <div class="col-md-6">
                <input id="mobile" name = "mobile" class="form-control" type="text" placeholder="手机号码" required="true"  minlength="11" maxlength="11" />
            </div>
            <div class="col-md-1">
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="row">
            <label class="form-label col-md-4">请输入密码: </label>
            <div class="col-md-6">
                <input id="password" name="password" class="form-control" type="password"  placeholder="密码" required="true" minlength="6" maxlength="16" />
            </div>
        </div>
    </div>

    <div class="row">
        <div class="col-md-5">
            <button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
        </div>
        <div class="col-md-5">
            <button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
        </div>
    </div>

</form>
</body>
<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>
</html>

2.2. 服务端: PASS = MD5(用户输入+随机Salt)

ValidatorUtil.java

public class ValidatorUtil {

    /**
     * 正则表达式
     */
    public static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");

    /**
     * 校验手机号格式是否正确
     * @param mobile
     * @return
     */
    public static boolean isMobile(String mobile) {
        if (StringUtils.isBlank(mobile)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(mobile);
        return m.matches();
    }

    public static void main(String[] args) {
        System.out.println(isMobile("18936095619"));
    }

}

MiaoshaUser.java

@Data
public class MiaoshaUser {
    private Long id;
    private String nickname;
    private String password;
    private String salt;
    private String head;
    private Date registerDate;
    private Date lastLoginDate;
    private Integer loginCount;
}

MiaoshaUserMapper.java

public interface MiaoshaUserMapper extends BaseMapper<MiaoshaUser> {
}

MiaoshaUserService.java

@Service
public class MiaoshaUserService {

    @Autowired
    private MiaoshaUserMapper miaoshaUserMapper;

    public Result<MiaoshaUser> findById(Long userId) {
        return Result.success(miaoshaUserMapper.selectById(userId));
    }

    public Result<CodeMsg> login(LoginVo loginVo) {
        if (loginVo == null) {
            return Result.fail(CodeMsg.SERVER_ERROR);
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
        if (user == null) {
            return Result.fail(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String dbSalt = user.getSalt();
        String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
        if (!StringUtils.equals(pwd, dbPass)) {
            return Result.fail(CodeMsg.PASSWORD_ERROR);
        }
        return Result.success(null);
    }
}

LoginController.java

@Controller
@RequestMapping("/login")
public class LoginController {

    private static final Logger log = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private MiaoshaUserService miaoshaUserService;

    @RequestMapping("/to_login")
    public String toLogin() {
        return "login";
    }

    @RequestMapping("/do_login")
    @ResponseBody
    public Result<CodeMsg> doLogin(LoginVo loginVo) {
        log.info(loginVo.toString());

        //参数校验
        String passForm = loginVo.getPassword();
        String mobile = loginVo.getMobile();
        if (StringUtils.isBlank(passForm)) {
            return Result.fail(CodeMsg.PASSWORD_BLANK);
        }
        if (StringUtils.isBlank(mobile)) {
            return Result.fail(CodeMsg.MOBILE_BLANK);
        }
        if (!ValidatorUtil.isMobile(mobile)) {
            return Result.fail(CodeMsg.MOBILE_ERROR);
        }
        //登录
        return miaoshaUserService.login(loginVo);
    }

}

新增CodeMsg

	//登录模块 5002XX
    SESSION_ERROR(500210, "Session不存在或者已经失效"),
    PASSWORD_BLANK(500211, "登录密码不能为空"),
    MOBILE_BLANK(500212, "手机号不能为空"),
    MOBILE_ERROR(500213, "手机号格式错误"),
    MOBILE_NOT_EXIST(500214, "手机号不存在"),
    PASSWORD_ERROR(500215, "密码错误");

3. JSR303参数校验+全局异常处理器

JSR303参数校验

导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.2</version>
</dependency>

新增CodeMsg

BIND_ERROR(500101,"参数校验异常:%s")

IsMobile.java

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {IsMobileValidator.class}
)
public @interface IsMobile {
    boolean required() default true;

    String message() default "手机号码格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

IsMobileValidator.java

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    @Override
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        int i = 1;
        if (required) {
            return ValidatorUtil.isMobile(s);
        }
        if (StringUtils.isBlank(s)) {
            return true;
        }
        return ValidatorUtil.isMobile(s);

    }
}

LoginVo.java

@Data
public class LoginVo {
    @NotNull
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min=32)
    private String password;
}

GlobalException.java

public class GlobalException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    private CodeMsg codeMsg;

    public GlobalException(CodeMsg cm) {
        this.codeMsg = cm;
    }

    public CodeMsg getCodeMsg() {
        return codeMsg;
    }
}

GlobalExceptionHandler.java

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Result<String> exceptionHandler(HttpServletRequest request, Exception e) {
        e.printStackTrace();
        if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return Result.fail(ex.getCodeMsg());
        } else if (e instanceof BindException) {
            BindException ex = (BindException) e;
            List<ObjectError> erros = ex.getAllErrors();
            ObjectError error = erros.get(0);
            String msg = error.getDefaultMessage();
            return Result.fail(CodeMsg.BIND_ERROR.fillArgs(msg));
        }
        return Result.fail(CodeMsg.SERVER_ERROR);
    }

}

MiaoshaUserService.java

@Service
public class MiaoshaUserService {

    @Autowired
    private MiaoshaUserMapper miaoshaUserMapper;

    public Result<CodeMsg> login(LoginVo loginVo) {
        if (loginVo == null) {
            int i = 1;
            throw new GlobalException(CodeMsg.SERVER_ERROR) ;
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
        if (user == null) {
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String dbSalt = user.getSalt();
        String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
        if (!StringUtils.equals(pwd, dbPass)) {
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        return Result.success(null);
    }
}

4. 通过redis实现分布式Session

生成token

判断用户的登录信息正确之后,我们生成一个随机的token,生成一个Cookie返回给浏览器,并保存一份到redis中
MiaoshaUserService.java

	public static final String COOKIE_NAME_TOKEN = "token";
	public MiaoshaUser getByToken(String token) {
        if (StringUtils.isBlank(token)) {
            return null;
        }
        MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
        if (user == null) {
            throw new GlobalException(CodeMsg.TOKEN_ERROR);
        }
        return user;
    }

	public Result<CodeMsg> login(HttpServletResponse response, LoginVo loginVo) {
        if (loginVo == null) {
            int i = 1;
            throw new GlobalException(CodeMsg.SERVER_ERROR) ;
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
        if (user == null) {
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String dbSalt = user.getSalt();
        String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
        if (!StringUtils.equals(pwd, dbPass)) {
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        //生成token
        String token = UUIDUtil.uuid();
        //设置redis中token的生存时间与cookie中的一致
        redisService.set(MiaoshaUserKey.token, token, user);
        Cookie cookie = new Cookie(COOKIE_NAME_TOKEN,token);
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
        cookie.setPath("/");
        response.addCookie(cookie);
        return Result.success(null);
    }

GoodController.java

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private MiaoshaUserService miaoshaUserService;

    @RequestMapping("/to_list")
    public String toList(Model model,
                         //兼容用户的cookie中传递的token或url参数中传递的token
                         @CookieValue(value = MiaoshaUserService.COOKIE_NAME_TOKEN, required = false) String cookieToken,
                         @RequestParam(value = MiaoshaUserService.COOKIE_NAME_TOKEN, required = false) String paramToken) {
        if (StringUtils.isBlank(cookieToken) && StringUtils.isBlank(paramToken)) {
            return "login";
        }
        String token = StringUtils.isBlank(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = miaoshaUserService.getByToken(token);
        model.addAttribute("user", user);
        return "goods_list";
    }

}

CodeMsg

TOKEN_ERROR(500216, "token错误");

MiaoshaUserKey.java

public class MiaoshaUserKey extends BasePrefix{

    public static final int TOKEN_EXPIRE = 3600 * 24;

    private MiaoshaUserKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }

    public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "token");
}

完善并优化

WebConfig.java

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserArgumentResolver userArgumentResolver;

    /**
     * 添加参数解析器
     * @param resolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

UserArgumentResolver.java

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private MiaoshaUserService miaoshaUserService;

    /**
     * 查看controller中是否接收MiaoshaUser类型的参数
     * @param parameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz == MiaoshaUser.class;
    }

    /**
     * 通过用户传递的token获取redis中存储的MiaoshaUser对象并返回赋值到controller中的参数中
     * @param parameter
     * @param mavContainer
     * @param webRequest
     * @param binderFactory
     * @return
     * @throws Exception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
        //寻找cookie中对应的token
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
        if (StringUtils.isBlank(cookieToken) && StringUtils.isBlank(paramToken)) {
            return null;
        }
        String token = StringUtils.isBlank(paramToken) ? cookieToken : paramToken;
        return miaoshaUserService.getByToken(response, token);
    }

    /**
     * 寻找cookie中存储的token
     * @param request
     * @param cookieName
     * @return
     */
    private String getCookieValue(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(cookieName)) {
                return cookie.getValue();
            }
        }
        return null;
    }
}

GoodsController.java

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private MiaoshaUserService miaoshaUserService;

    @RequestMapping("/to_list")
    public String toList(Model model, MiaoshaUser user) {
        model.addAttribute("user", user);
        return "goods_list";
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值