4.开发社区登录模块

 

application.properties中配置邮箱信息(发送方)

#MailProperties
# 使用的邮箱对应的smtp服务器地址
spring.mail.host=smtp.163.com 
# 邮箱信息
spring.mail.username=cjhtxdy@163.com
spring.mail.password=WSSFFVTWDBLLJDDZ
# smtp协议相关配置
spring.mail.properties.mail.smtl.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

写一个工具类MailClient并测试

@Component
public class MailClient {
    private static final Logger logger= (Logger) LoggerFactory.getLogger(MailClient.class);

    @Autowired
    private JavaMailSender mailSender;

    @Value("$spring.mail.username")//从application里注入
    private String from; //邮件发送人

    //to:发送目标 subject:邮件主题  content:邮件内容
    public void setMailSender(String to,String subject,String content){
        try {
            MimeMessage message=mailSender.createMimeMessage();
            MimeMessageHelper helper=new MimeMessageHelper(message);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content,true);
        } catch (MessagingException e) {
            logger.info("发送邮件失败:"+e.getMessage());
        }
    }
}
@Test
public void testTextMail(){
    mailClient.sendMail("接收方@qq.com","TEST","welcome.");
}

 

 (1)首页跳转注册页面

首页头部【注册】按钮,点击跳转
<li class="nav-item ml-3 btn-group-vertical">
     <a class="nav-link" th:href="@{/register}">注册</a>
</li>

(2)LoginController

@Controller
public class LoginController {
    @RequestMapping(path = "/register",method = RequestMethod.GET)
    public String getRegisterPage(){//获取首页
        return "/site/register";
    }
}

(3)一些准备

commons lang依赖:对字符串、集合等做一些数据检查

#community链接可能在开发、上线等地址不同,要做成可配置的
community.path.domain=http://localhost:8080

新建工具类:CommunityUtil
生成随机字符串、MD5加密(只能加密不能解密)+随机盐
public class CommunityUtil {
//生成随机字符串
public static String generateUUID(){
return UUID.randomUUID().toString().replaceAll("-","");
}
//MD5加密(安全性不如SHA1,都不如SHA2,sha1输出160位消息摘要 sha2输出256位消息摘要。sha2的碰撞的概率比sha1要低,因为sha2有2^256种组合sha1有2^160种组合。)
//hello---->abc123456
//helld + 随机盐(随机字符串)--->abc123456ghk
public static String md5(String key){
if(StringUtils.isBlank(key)){
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
存储用户密码应该使用什么加密算法:
    算法需不可逆,这样才能有效防止密码泄露。
    算法需相对慢,可以动态调整计算成本,缓慢是应对暴力破解有效方式。

旧的加密:
    过去密码加密常用MD5或者SHA。MD5是早期设计的加密哈希,它生成哈希速度很快,随着计算机能力的增强,出现了被破解的情况,所以又有了一些长度增大的哈希函数,如:SHA-1,SHA-256等。下面是它们的一些比较:
    MD5:速度快生成短哈希(16 字节)。意外碰撞的概率约为:1.47*10^(-29)。
SHA1:比 md5 慢 20%,生成的哈希比 MD5 长一点(20 字节)。意外碰撞的概率约为:1*10^(-45)。
SHA256:最慢,通常比 md5 慢 60%,并且生成的哈希长(32 字节)。意外碰撞的概率约为:4.3*10^(-60)。
    为了确保安全你可能会选择目前长度最长的哈希SHA-512,但硬件能力在增强,或许有一天又会发现新的漏洞,研究人员又推出较新的版本,新版本的长度也会越来越长,而且他们也可能会发布底层算法,所以我们应该另外寻找更合适的算法。

加盐操作:
    密码安全,除了要选择足够可靠的加密算法外,输入数据的强度也要提升,因为密码是人设置的,其字符长度组合强度不可能一致,如果直接进行哈希存储往往会提升爆破的概率,这时我们需要加盐。加盐是密码学中经常提到的概念,其实就是随机数据。

目前来看有这么几个算法 PBKDF2、 BCrypt 和 SCrypt 可以满足。
    Bcrypt 是基于 eksblowfish 算法设计的加密哈希函数,它最大的特点是:可以动态调整工作因子(迭代次数)来调整计算速度,因此就算以后计算机能力不断增加,它仍然可以抵抗暴力攻击。(spring security BCryptPasswordEncoder)
    密码存储这种场景下,将密码哈希处理是最好的方式,第一它本身就是加密哈希函数,其次按照摩尔定律的定义,集成系统上每平方英寸的晶体管数量大约每 18 个月翻一番。在 2 年内,我们可以增加它的工作因子以适应任何变化。
https://learn.skyofit.com/archives/2487

MD5加密 改成 bCrypt加密(虽然BCrypt也是输入的字符串+盐,但是与MD5+盐的主要区别是:每次加的盐不同,导致每次生成的结果也不相同。)
//bCrypt加密:自动加盐
public static String bCrypt(String password){
if(StringUtils.isBlank(password)){
return null;
}
return BCrypt.hashpw(password,BCrypt.gensalt());
}

(4)UserService:注册逻辑

//注册:
    //返回错误信息:账号不存在等
    public Map<String,Object> register(User user){
        Map<String,Object> map=new HashMap<>();
        //1.参数判断
        //空值处理(这里其实前端会做处理,这里基本走不到,但是一些无效行为可以到此,比如username为"  ")
        if(user==null){
            throw new IllegalArgumentException("参数不能为空!");
        }
        if(StringUtils.isBlank(user.getUsername())){
            map.put("usernameMsg","账号不能为空");
            return map;
        }
        if(StringUtils.isBlank(user.getPassword())){
            map.put("passwordMsg","密码不能为空");
            return map;
        }
        if(StringUtils.isBlank(user.getEmail())){
            map.put("emailMsg","邮箱不能为空");
            return map;
        }
        //验证账号
        User u=userMapper.selectByName(user.getUsername());//数据库里查到的用户
        if(u!=null){
            map.put("usernameMsg","该账号已存在!");
            return map;
        }
        //验证邮箱
        u=userMapper.selectByEmail(user.getEmail());//数据库里查到的用户
        if(u!=null){
            map.put("emailMsg","该邮箱已被注册!");
            return map;
        }

        //2.注册用户
        user.setPassword(CommunityUtil.bCrypt(user.getPassword()));//加密密码
        user.setType(0);//默认普通用户
        user.setStatus(0);//账号未激活
        user.setActivationCode(CommunityUtil.generateUUID());//激活码
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));//随机头像
        user.setCreateTime(new Date());
        userMapper.insertUser(user);

        //3.激活邮件
        Context context=new Context();
        context.setVariable("email",user.getEmail());
        //激活链接
        String url=domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode();
        context.setVariable("url",url);
        String content=templateEngine.process("/mail/activation",context);
        mailClient.sendMail(user.getEmail(),"激活账号",content);
        return map;
    }

(5)注册后与前端交互

 @RequestMapping(path = "/register",method = RequestMethod.POST)//注意这里是POST,提交表单
    public String register(Model model, User user){
        Map<String,Object> map=userService.register(user);//拿到service返回的错误信息
        if(map==null&&map.isEmpty()){//错误信息为空,发激活邮件
            model.addAttribute("msg","注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");
            model.addAttribute("target","/index");//跳转至首页
            return "/site/operate-result";//model+此模板=返回view
        }else{//携带错误信息,直接返回到注册页面上显示
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            return "/site/register";
        }
    }
//前端部分需要注意:
页面跳转时,用户名和密码都要带着
页面显示错误信息(获取变量)
确认密码部分直接前端判断即可

问题:

激活邮件发送失败后,这时候再注册,用户会显示已注册
而且注册时,如果是在邮件发送时期出现问题,就会重复插入,下次再查询,数据库直接报错(select one失败)
所以在注册时,需要先看status是否是0 0则未激活,还可以进行注册,删除原记录,重新注册 1已激活,可以使用其他功能
if(u!=null&&u.getStatus()==0){//只是未激活
//则删除原记录,重新注册
userMapper.deleteUser(user.getUsername());
}

(6)激活账号:重复激活、status改变为1激活成功、激活码伪造则失败

public int activation(int userId,String code){
        User user=userMapper.selectById(userId);
        if(user.getStatus()==1){//重复激活
            return ACTIVATION_REPEAT;
        }else if(user.getActivationCode().equals(code)){//激活码正确,激活成功
            userMapper.updateStatus(userId,1);
            return ACTIVATION_SUCCESS;
        }else{//激活失败
            return ACTIVATION_FAILURE;
        }
    }

 

 分布式session共享方案:
1、粘性session:在nginx中提供一致性哈希策略,可以保持用户ip进行hash值计算固定分配到某台服务器上,负载也比较均衡,其问题是假如有一台服务器挂了,session也丢失了。
2、同步session:当某一台服务器存了session后,同步到其他服务器中,其问题是同步session到其他服务器会对服务器性能产生影响,服务器之间耦合性较强。
3、共享session:单独搞一台服务器用来存session,其他服务器都向这台服务器获取session,其问题是这台服务器挂了,session就全部丢失。
4、redis集中管理session(主流方法):redis为内存数据库,读写效率高,并可在集群环境下做高可用。 

JWT和cookie/session的区别及优缺点

 Spring Boot没有为Kaptcha提供自动配置,但是可以通过配置类和其他配置手动集成Kaptcha到Spring Boot应用中。Spring Framework本身并不为Kaptcha提供自动配置。

@Configuration
public class KaptchaConfig {//验证码配置类
    @Bean
    public Producer kaptchaProducer() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "100");
        properties.setProperty("kaptcha.image.height", "40");
        properties.setProperty("kaptcha.textproducer.font.size", "32");
        properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

<div class="col-sm-4">
    <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
    <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
<script>
    function refresh_kaptcha() {
        var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
        $("#kaptcha").attr("src", path);
    }
</script>

 5.登录和退出

(1)登录凭证

@Data
//登陆凭证
public class LoginTicket {
    private int id;
    private int userId;
    private String ticket;
    private int status;
    private Date expired;
}

(2)LoginTicketMapper

@Mapper
public interface LoginTicketMapper {
    @Insert({"insert into login-ticket(user_id,ticket,status,expired) " +
            "values(#{userId},#{ticket},#{status},#{expired})"})
    @Options(useGeneratedKeys = true,keyProperty = "id")
    int insertLoginTicket(LoginTicket loginTicket);

    @Select({ "select id,user_id,ticket,status,expired ",
            "from login_ticket where ticket=#{ticket}"})
    LoginTicket selectByTicket(String ticket);

    @Update({"<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket}",
            "<if test=\"ticket!=null\">",
            "and 1=1",
            "</if>",
            "</script>"})
    int updateStatus(String ticket, int status);
}

(3)service 登录逻辑:验证是否有该用户,密码是否正确

//5.登录逻辑
    public Map<String,Object> login(String username,String password,int expiredSeconds){
        Map<String,Object> map=new HashMap<>();
        //空值处理
        if(StringUtils.isBlank(username)){
            map.put("usernameMsg","账号不能为空");
            return map;
        }
        if(StringUtils.isBlank(password)){
            map.put("passwordMsg","密码不能为空");
            return map;
        }
        //验证账号
        User user=userMapper.selectByName(username);//数据库里查到的用户
        if(user==null){
            map.put("usernameMsg","该账号不存在!");
            return map;
        }
        if(user.getStatus()==0){//未激活
            map.put("usernameMsg","该账号未激活!");
            return map;
        }
        //验证密码
        boolean flag=CommunityUtil.checkPassword(password,user.getPassword());
        if(!flag){
            map.put("passwordMsg","密码不正确");
            return map;
        }
        //生成登陆凭证
        LoginTicket loginTicket=new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());//凭证
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000));
        loginTicketMapper.insertLoginTicket(loginTicket);
        map.put("ticket",loginTicket.getTicket());
        return map;
    }

(4)编写表现层(Controller)层的逻辑,成功时给客户端发送一个cookie,失败时跳转回登录页

 @RequestMapping(path = "/login",method = RequestMethod.POST)
    public String login(String username,String password,String code,boolean rememberme,
                        Model model,HttpSession session,HttpServletResponse response){
        //检查验证码
        String kaptcha=(String)session.getAttribute("kaptcha");
        if(StringUtils.isBlank(kaptcha) ||StringUtils.isBlank(code) ||!kaptcha.equalsIgnoreCase(code) ){
            model.addAttribute("codeMsg","验证码不正确");
            return "/site/login";
        }
        int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;//凭证失效时间
        Map<String,Object> map=userService.login(username,password,expiredSeconds);//拿到service返回的错误信息
        if(map.containsKey("ticket")){//成功有登录凭证,放至cookie
            Cookie cookie=new Cookie("ticket",map.get("ticket").toString());
            cookie.setPath(contextPath);
            cookie.setMaxAge(expiredSeconds);
            response.addCookie(cookie);
            return "redirect:/index";
        }else{//携带错误信息,直接返回到登录页面上显示
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("passwordMsg",map.get("passwordMsg"));
            return "/site/login";
        }
    }

(5)修改对应html

        <div class="main">
            <div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3">
                <h3 class="text-center text-info border-bottom pb-3">&nbsp;&nbsp;</h3>
                <form class="mt-5" method="post" th:action="@{/login}">
                    <div class="form-group row">
                        <label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
                        <div class="col-sm-10">
                            <input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
                                   th:value="${param.username}"
                                   id="username" name="username" placeholder="请输入您的账号!" required>
                            <div class="invalid-feedback" th:text="${usernameMsg}">
                                该账号不存在!
                            </div>
                        </div>
                    </div>
                    <div class="form-group row mt-4">
                        <label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
                        <div class="col-sm-10">
                            <input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
                                   th:value="${param.password}"
                                   id="password" name="password" placeholder="请输入您的密码!" required>
                            <div class="invalid-feedback" th:text="${passwordMsg}">
                                密码长度不能小于8位!
                            </div>
                        </div>
                    </div>
                    <div class="form-group row mt-4">
                        <label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
                        <div class="col-sm-6">
                            <input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"
                                   id="verifycode" name="code" placeholder="请输入验证码!">
                            <div class="invalid-feedback" th:text="${codeMsg}">
                                验证码不正确!
                            </div>
                        </div>
                        <div class="col-sm-4">
                            <img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
                            <a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
                        </div>
                    </div>
                    <div class="form-group row mt-4">
                        <div class="col-sm-2"></div>
                        <div class="col-sm-10">
                            <!--name="rememberme" 和后端对应-->
                            <input type="checkbox" id="remember-me" name="rememberme"
                                   th:checked="${param.rememberme}">
                            <label class="form-check-label" for="remember-me">记住我</label>
                            <a href="forget.html" class="text-danger float-right">忘记密码?</a>
                        </div>
                    </div>
                    <div class="form-group row mt-4">
                        <div class="col-sm-2"></div>
                        <div class="col-sm-10 text-center">
                            <button type="submit" class="btn btn-info text-white form-control">立即登录</button>
                        </div>
                    </div>
                </form>                
            </div>
        </div>

(6)登录退出:拿到cookie中ticket信息,修改数据库中该ticket的状态status,重定向到登陆页面

@RequestMapping(path="/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket")String ticket){
     userService.logout(ticket);
     return "redirect:/login";
}
 //登录退出
public void logout(String ticket){
     loginTicketMapper.updateStatus(ticket,1);//修改该凭证的登陆状态
}

(7)login_ticket表中很多ticket超时时要清除

 5.显示登录信息

1. 拦截器概念
    动态拦截Actioon调用的对象,使开发者在一个Actioon执行的前后执行一段代码,也可以在Action执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。
2. 作用
    动态拦截Action调用的对象(也就是实际项目中的controller层的接口)
3. 适用场景
3.1. 用户登录校验
3.2. 资源拦截,阻止访问
4. 实现拦截器
    实现拦截器需要实现HandlerInterceptor接口,HandlerInterceptor接口中有三个方法preHandle、postHandle、afterCompletion。
4.1. preHandle
    在Action执行前调用
4.2. postHandle
    在Action执行后调用,生成视图前调用
4.3. afterCompletion
    在DispatcherServlet完全处理完请求之后被调用,可用于清理资源

(1)需要实现的功能:

在请求开始时查询登录用户,在本次请求中持有用户数据,在模板视图上显示用户数据,在请求结束时清理用户数据。登录后,显示用户的逻辑如下图所示
image-20220426224723104

(2)以定义拦截器,实现HandlerInterceptor

页面拦截器处理的是请求,属于表现层的逻辑,在Controller包下新建包Interceptor,新建LoginTicketInterceptor类实现HandleInterceptor接口

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
    @Autowired
    private UserService userService;
    @Autowired
    private HostHolder hostHolder;

    //在请求开始时查询登录用户
    //在本次请求中持有用户数据
    @Override //在Action执行前调用
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从cookie中取出凭证,查看是否过期,未过期可以直接放行,过期需要重新登录
        String ticket= CookieUtil.getValue(request,"ticket");
        if(ticket!=null){
            LoginTicket loginTicket=userService.findLoginTicket(ticket);
            //检查该凭证是否有效:状态是否是登录状态(0),是否超时
            if(loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){//如果登出了,就再也进不去(问题!)//如果Cookie清了,就再也进不去(问题!)
          //有效,则在本次请求中持有用户数据 User user=userService.findUserById(loginTicket.getUserId()); hostHolder.setUsers(user);//user被持有 } } return true; } //在模板视图上显示用户数据 @Override//在Action执行后调用,生成视图前调用 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { User user=hostHolder.getUser(); if(user!=null&&modelAndView!=null){ modelAndView.addObject("loginUser",user); } } //在请求结束时清理用户数据 @Override//在DispatcherServlet完全处理完请求之后被调用,可用于清理资源 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){ hostHolder.clear(); } }
这里有两个问题:
如果登出了,就再也进不去
如果Cookie清了,就再也进不去
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
    <a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}">
    <a class="nav-link" th:href="@{/register}">注册</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}">
    <a class="nav-link" th:href="@{/login}">登录</a>
</li>

(3)配置拦截器,指定拦截路径

写一个配置类,在之前写配置类主要是想在配置类中声明一个第三方Bean,将其装配到容器中,拦截器的逻辑和别的不太一样,要求在配置类中实现一个接口而不是简单装配一个Bean。通过以下代码reqistry实现对拦截器的注入。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AlphaInterceptor alphaInterceptor;
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)
                .excludePathPatterns("/css/*","/js/*","/img/*")
                .addPathPatterns("/register","/login");

        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/css/*","/js/*","/img/*");
    }
}

先在util包下建一个在工具类,封装从request中获取cookie的方法

public class CookieUtil {
    public static String getValue(HttpServletRequest request,String name){
        if(request==null||name==null){
            throw new IllegalArgumentException("参数为空!");
        }
        Cookie[] cookies=request.getCookies();
        if(cookies!=null){
            for(Cookie cookie:cookies){
                if(cookie.getName().equals(name)){//cookie中该用户的信息
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

通过获取的cookie中的信息,查询LoginTicket,并从中获取用户信息,在这之前需要在服务层先编写通过cookie中的信息获取LoginTicket的方法

public LoginTicket findLoginTicket(String ticket){
    return loginTicketMapper.selectByTicket(ticket);
}

在存储用户信息时需要考虑多线程的情况,不然会在并发的时候产生冲突,必须考虑线程隔离。java中的ThreadLocal工具可以解决该问题。新建一个工具类HostHolder,该类的起到一个容器的作用

 //持有用户信息,用于代替session对象
@Component
public class HostHolder {
    private ThreadLocal<User> users = new ThreadLocal<>();
    public void setUsers(User user) {
        users.set(user);
    }
    public User getUser(){
        return users.get();
    }
    public void clear(){
        users.remove();
    }
}

 

6.账号设置:

  首先请求必须是一个 POST 请求,其次表单的属性 enctype = “multipart/form-data”

  然后就是利用 MultipartFile 处理上传文件。

  然后就是访问账号设置页面,上传头像,获取头像。

(1)头像上传之后是存放到我们的服务器硬盘之上,所以需要在 application.properties配置一下资源上传之后的存放路径。

community.path.upload=f:/nowcoder/data/upload

  上传完文件最终是需要更新用户的 HeaderUrl,所以 Service 就需要提供一个方法改变这个 URL,然后上传文件的事情就在 Controller 里面解决掉,业务层只解决更新路径的这个业务就可以了。

(2)在 UserService 里面追加一个方法更新用户的 URL:

public int updateHeader(int userId,String headerUrl){
     return userMapper.updateHeader(userId,headerUrl);
}

(3)上传头像:
MultipartFile是SpringMVC提供简化上传操作的工具类。

 //上传头像
    @RequestMapping(path="/upload",method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model){//MultipartFile是SpringMVC提供简化上传操作的工具类。
        if(headerImage==null){
            model.addAttribute("error","您还未选择图片!");
            return "/site/setting";
        }
        String fileName=headerImage.getOriginalFilename();
        String suffix=fileName.substring(fileName.lastIndexOf("."));
        if(StringUtils.isBlank(suffix)){
            model.addAttribute("error","文件格式不正确");
            return "/site/setting";
        }

        //生成随机文件名
        fileName = CommunityUtil.generateUUID() + suffix;
        //确定文件的存放路径
        File dest = new File(uploadPath + "/" +fileName);
        try {
            //存储文件
            headerImage.transferTo(dest);
        } catch (IOException e) {
            logger.error("上传文件失败" + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!",e);
        }

        //更新当前用户的头像的网址
        User user = hostHolder.getUser();
        String headerUrl = domain + contextPath +"/user/header/" + fileName;
        userService.updateHeader(user.getId(),headerUrl);

        return "redirect:/index";
    }

    //获取头像
    @RequestMapping(path="/header/{fileName}",method = RequestMethod.GET)
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response){
        //服务器存放路径
        fileName=uploadPath+"/"+fileName;
        //文件后缀
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        response.setContentType("image/" + suffix);

        try (
                FileInputStream fis = new FileInputStream(fileName);
                OutputStream os = response.getOutputStream();
        ){
            byte[] buffer = new byte[1024];
            int b = 0;
            while((b = fis.read(buffer)) != -1){
                os.write(buffer,0, b);
            }
        }  catch (IOException e) {
            logger.error("读取头像失败" + e.getMessage());
        }
    }

(4)修改密码:(有问题,待修正)

验证原始密码:给用户输入的旧密码加密,验证是否和数据库中密码一致

更新密码:给新密码加密,入库

// 修改密码 Controller
@LoginRequired
@RequestMapping(path = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(String oldPassword, String newPassword, Model model) {
    User user = hostHolder.getUser();
    Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword);
    if (map == null || map.isEmpty()) {
        return "redirect:/logout";
    } else {
        model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg"));
        model.addAttribute("newPasswordMsg", map.get("newPasswordMsg"));
        return "/site/setting";
    }
}
 
// 修改密码 Service
    public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (StringUtils.isBlank(oldPassword)) {
            map.put("oldPasswordMsg", "原密码不能为空!");
            return map;
        }
        if (StringUtils.isBlank(newPassword)) {
            map.put("newPasswordMsg", "新密码不能为空!");
            return map;
        }

        // 验证原始密码
        User user = userMapper.selectById(userId);
        oldPassword = CommunityUtil.bCrypt(oldPassword);
        if (!user.getPassword().equals(oldPassword)) {
            map.put("oldPasswordMsg", "原密码输入有误!");
            return map;
        }

        // 更新密码
        newPassword = CommunityUtil.bCrypt(newPassword);
        userMapper.updatePassword(userId, newPassword);

        return map;
    }

 

7.检查登录状态

  需要解决的问题:防止直接在浏览器输入网址进入“登录信息设置”的网页。
  解决方案:设置拦截器,拦截所有请求,且在指定的方法上进行拦截。那么可以自定义注解,让拦截器只拦截带有这个自定义注解的方法。

使用拦截器

  • 在方法前标注自定义注解
  • 拦截所有请求,只处理带有该注解的方法
  • 自定义注解
    • 常用的元注解:
      @Target、@Retention、@Document、@Inherited
  • 如何读取注解:
    Method.getDeclaredAnnotations ()
    Method.getAnnotation (Class annotationClass)

 (1)自定义注解,是否登录:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {//打了这个标记,登录时才能访问
}

使用注解:算是一个权限控制,这两个请求必须登录

@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
     return "/site/setting";
}
@LoginRequired
@RequestMapping(path = "/upload", method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
     if (headerImage == null) {
          model.addAttribute("error", "您还没有选择图片!");
          return "/site/setting";
    }
}

(2)拦截器

  看拦截目标的类型是不是方法HandlerMethod,如果是,转型成handlerMethod,获取method对象,

  获取注解,注解不为空loginRequired != null即带有这个注解(LoginRequired.class),即需要登录,

  若此时hostHolder.getUser()==null没登录,就要重定向到登录页面(即项目路径+/login),返回false,不能继续执行该方法。

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
    @Autowired
    private HostHolder hostHolder;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            HandlerMethod handlerMethod=(HandlerMethod) handler;
            Method method=handlerMethod.getMethod();
            LoginRequired loginRequired=method.getAnnotation(LoginRequired.class);
            if(loginRequired!=null&&hostHolder.getUser()==null){
                response.sendRedirect(request.getContextPath()+"/login");//return ”redirect“底层其实也是这个,可以通过配置文件注入路径参数,也可以从请求中直接取到路径
                return false;
            }
        }
        return true;
    }
}

拦截器配置和之前一样,要注入拦截器、排除静态路径。

WebMvcConfig

@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginRequiredInterceptor)
            .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}

 

posted @ 2023-11-13 23:52  壹索007  阅读(14)  评论(0编辑  收藏  举报