springboot 先後端分離開發 從零到整(3、登陸以及登陸狀態的持續)

今天來寫一下怎麼登陸和維持登陸狀態。前端

相信登陸驗證你們都比較熟悉,在Javaweb中通常保持登陸狀態都會用session。但若是是先後端分離的話,session的做用就沒有那麼明顯了。對於先後端分離的項目目前比較流行的是jwt驗證。參考文章:https://blog.csdn.net/qq_27828675/article/details/80923678java

其實,做爲開發一整個項目來講,以我一年多開發經驗來,建議你們先作個需求開發文檔,把項目的業務大體構思一下。而後再統一把數據庫設計好,把用到的表和字段都建好,能夠增長開發速率的。web

好了,廢話少說,開始講解一下怎麼作登陸驗證過程吧。ajax

首先,先建立token生成工具類。token能夠設置過時時間,我項目中設置的是10個小時後過時。算法

添加依賴環境。spring

<!--JJWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

工具類。在生成token的時候我把用戶郵箱注入token中,方便根據用戶郵箱查詢用戶信息。祕鑰暫時用的是後臺寫死,也能夠用用戶密碼做爲每個token的祕鑰。數據庫

package com.liao.tdoor.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;

/**
 * 生成token的工具類
 */
public class tokenUtil {
    /**
     * 簽名祕鑰(惟一祕鑰,能夠用密碼作爲祕鑰)
     */
    public static final String SECRET="admin";

    /**
     * 生成token
     * @param username
     * @return
     */
    public static String createJwtToken(String username){
        String issuer="tdoor";
        String subject="liao";
        long ttlMillis=36000000;//10個小時後過時
        return createJwtToken(username,issuer,subject,ttlMillis);
    }

    /**
     * 生成token
     * @param username 用戶名
     * @param issuer 改JWT的簽發者,是否使用能夠選
     * @param subject 改JWT所面向的用戶,是否使用可選
     * @param ttlMillis 簽發時間(有效時間,過時會報錯)
     * @return token string
     */
    public static String createJwtToken(String username,String issuer,String subject,long ttlMillis){
        //簽名算法,將token進行簽名
        SignatureAlgorithm signatureAlgorithm=SignatureAlgorithm.HS256;
        //生成簽發時間
        long nowMills=System.currentTimeMillis();
        Date now=new Date(nowMills);
        //經過祕鑰簽名JWT
        byte[] apiKeySecretBytes= DatatypeConverter.parseBase64Binary(SECRET);
        Key signingKey=new SecretKeySpec(apiKeySecretBytes,signatureAlgorithm.getJcaName());
        //建立token
        JwtBuilder builder=Jwts.builder().setId(username)
                                .setIssuedAt(now)
                                .signWith(signatureAlgorithm,signingKey);
        //添加過時時間
        if(ttlMillis>=0){
            long expMillis=nowMills+ttlMillis;
            Date exp=new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }
    //驗證和讀取JWT的示例方法
    public static Claims parseJWT(String jwt){
        Claims claims=Jwts.parser()
                        .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
                        .parseClaimsJws(jwt).getBody();
        return claims;
    }
    public static void main(String[] args){
        System.out.println(tokenUtil.createJwtToken("liao180@vip.qq.com"));
    }
}
View Code

而後是用戶登陸驗證。apache

package com.liao.tdoor.dao;

import com.liao.tdoor.model.User;
import com.liao.tdoor.model.UserSign;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectKey;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import java.util.Date;

@Repository
public interface UserDao {
    /**
     * @desc 查詢改郵箱是否被註冊
     * @param email
     * @return
     */
    @Select("select * from user where email=#{email}")
    public User isExistUser(String email);
    /**
     * 用戶註冊
     * @param user
     */
    @Insert("insert into user(id,email,password,nickname) values (#{id},#{email},#{password},#{nickname})")
    @SelectKey(keyProperty = "id", resultType = String.class, before = true, statement = "select replace(uuid(), '-', '') as id from dual")
    public void addUser(User user);
    /**
     * 用戶登陸
     * @param email
     * @param password
     * @return
     */
    @Select("select * from user where email=#{email} and password=#{password}")
    public User login(String email,String password);

    /**
     * 經過ID查詢用戶信息
     * @param user_id
     * @return
     */
    @Select("select * from user where id=#{user_id}")
    public User QueryInfoById(String user_id); 
}
View Code

用戶登陸成功後生成token,把token返回給客戶端。json

package com.liao.tdoor.service;

import com.liao.tdoor.dao.CodeDao;
import com.liao.tdoor.dao.PostingDao;
import com.liao.tdoor.dao.UserDao;
import com.liao.tdoor.model.Posting;
import com.liao.tdoor.model.User;
import com.liao.tdoor.model.UserSign;
import com.liao.tdoor.model.VerificationCode;
import com.liao.tdoor.responseMsg.PersonalEntity;
import com.liao.tdoor.responseMsg.RespCode;
import com.liao.tdoor.responseMsg.RespEntity;
import com.liao.tdoor.util.DateUtils;
import com.liao.tdoor.util.RandomTools;
import com.liao.tdoor.util.SendEmailUtils;
import com.liao.tdoor.util.tokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 用戶服務層
 * @author 廖某某
 * @date 2019/02/17
 */
@Service
public class UserService {
    @Autowired
    UserDao userDao;
    @Autowired
    CodeDao codeDao;
    @Autowired
    SendEmailUtils sendEmailUtils;
    @Autowired
    PostingDao pDao;

    private RespEntity respEntity=new RespEntity();

    private User user=new User();

    private VerificationCode verificationCode=new VerificationCode();

    private PersonalEntity infoEntity=new PersonalEntity();
    /**
     * 發送驗證碼
     * @param email
     * @return
     */
    public RespEntity sendCode(String email){
        try{
            String code= RandomTools.randomCode();//產生隨機的驗證碼
            User user=new User();
            user=userDao.isExistUser(email);
            if(user==null){
                System.out.println("郵箱:"+email+"--驗證碼爲:"+code);
                //修改數據庫中的驗證碼
                verificationCode=codeDao.checkCode(email);
                if(verificationCode!=null){
                    codeDao.changeCode(email,code);
                }
                //發送郵件開始 發送驗證碼
                sendEmailUtils.sendRegisterCode(email,code);
                //保存驗證碼信息到數據庫
                codeDao.saveCode(email,code,new Date());

                respEntity=new RespEntity(RespCode.REGISTER_SEND);
            }else {
                respEntity= new RespEntity(RespCode.REGISTER_NOTS);
            }

        }catch (Exception e){
            e.printStackTrace();
        }
        return respEntity;
    }
    /**
     * 註冊信息提交
     * @param email
     * @param nickName
     * @param password
     * @param registerCode
     * @return
     */
    public RespEntity RegisterInfo(String email,String nickName,String password,String registerCode){
        verificationCode=codeDao.checkCode(email);
        if(verificationCode!=null){
            if(registerCode.equals(verificationCode.getCode())){
                //時間校驗--暫略
                User user=new User(email,password,nickName);
                userDao.addUser(user);
                //刪除驗證碼信息
                codeDao.deleteCode(email);
                respEntity=new RespEntity(RespCode.REGISTER_SUCCESS);
            }else {
                respEntity=new RespEntity(RespCode.CODE_EXPIRED);
            }
        }else {
            respEntity=new RespEntity(RespCode.REGISTER_FAILED);
        }
        return respEntity;
    }

    /**
     * 登陸驗證
     * @param email
     * @param password
     * @return
     */
    public RespEntity Login(String email,String password){
        user=userDao.login(email,password);
        String token="";
        if(user!=null){
            token= tokenUtil.createJwtToken(email);
            respEntity=new RespEntity(RespCode.LOGIN_SUCCESS,token);
        }else {
            respEntity=new RespEntity(RespCode.LOGIN_FAILED);
        }
        return respEntity;
    }
    /**
     * 根據舊密碼更改密碼
     * @param usedPassword
     * @return
     */
    public RespEntity ChangePassword(String email,String usedPassword,String newPassword){
        user=userDao.login(email,usedPassword);
        if(user==null){
            respEntity=new RespEntity(RespCode.PASSWORD_FAILED);
        }else {
            userDao.ChangePassword(email,newPassword);
            respEntity=new RespEntity(RespCode.SUCCESS);
        }
        return respEntity;
    }
}
View Code

controller。後端

package com.liao.tdoor.controller;

import com.liao.tdoor.annotation.CurrentUser;
import com.liao.tdoor.annotation.PassToken;
import com.liao.tdoor.annotation.UserLoginToken;
import com.liao.tdoor.model.User;
import com.liao.tdoor.responseMsg.PersonalEntity;
import com.liao.tdoor.responseMsg.RespEntity;
import com.liao.tdoor.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

/**
 * @author 廖某某
 * 用戶控制層
 */
@RestController
public class userController {

    @Autowired
    UserService userService;
    private RespEntity respEntity=new RespEntity();
    private PersonalEntity pEntity=new PersonalEntity();

    @RequestMapping("register")
    public RespEntity register(@RequestBody Map<String,Object> map){
        String e_mail=(String)map.get("email");
        String nickName=(String)map.get("nickName");
        String password=(String)map.get("password");
        String registerCode=(String)map.get("code");
        respEntity=userService.RegisterInfo(e_mail,nickName,password,registerCode);
        return respEntity;
    }
    @RequestMapping("sendCode")
    public RespEntity sendPollCode(@RequestBody Map<String,Object> map){
        String email=(String)map.get("email");
        RespEntity respEntity=userService.sendCode(email);
        return respEntity;
    }
    @RequestMapping("/login")
    public RespEntity testData(@RequestBody Map<String,Object> map){
        String email=(String)map.get("email");
        String password=(String)map.get("password");
        respEntity=userService.Login(email,password);
        return respEntity;
    }
}
View Code

登陸操做完成後,客戶端根據token來進行請求操做。前端是ajax請求,通常在請求頭部攜帶token,而後服務端攔截請求,獲取token並進行驗證,判斷有無和是否過時,若是token不過時,則放行。

三個註解

注入當前登陸用戶註解

package com.liao.tdoor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 注入當前用戶
 * @author 廖某某
 * @date 2019/02/18
 * 在Controller的方法參數中使用此註解,該方法在映射時會注入當前登陸的User對象
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

須要登陸的標記註解

package com.liao.tdoor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 須要進行登陸才能進行操做的註解
 * @author 廖某某
 * @date 2019/02/8
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

不須要登陸的標記註解

package com.liao.tdoor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 跳過驗證
 * @author 廖某某
 * @date 2019/02/18
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

建立攔截器。攔截器攔截token並解析token中的用戶郵箱,查詢用戶信息並注入到CurrentUser 中。

package com.liao.tdoor.interceptor;

import com.liao.tdoor.annotation.PassToken;
import com.liao.tdoor.annotation.UserLoginToken;
import com.liao.tdoor.dao.UserDao;
import com.liao.tdoor.model.User;
import com.liao.tdoor.responseMsg.CurrentUserConstants;
import com.liao.tdoor.util.tokenUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 攔截器,攔截token
 * @author 廖某某
 * @date 2019/02/18
 */
public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    UserDao userDao;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest,
                             HttpServletResponse httpServletResponse,
                             Object object){
        //設置容許哪些域名應用進行ajax訪問
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", " Origin, X-Requested-With, content-Type, Accept, Authorization");
        httpServletResponse.setHeader("Access-Control-Max-Age","3600");
        //獲取請求頭的token
        String token=httpServletRequest.getHeader("Authorization");
        //若是不是映射到方法直接經過
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod) object;
        Method method=handlerMethod.getMethod();
        //檢查是否有passToken註釋,有則跳過驗證
        if(method.isAnnotationPresent(PassToken.class)){
            PassToken passToken=method.getAnnotation(PassToken.class);
            if(passToken.required()){
                return true;
            }
        }
        //檢查是否有須要用戶權限的註解
        if(method.isAnnotationPresent(UserLoginToken.class)){
            UserLoginToken userLoginToken=method.getAnnotation(UserLoginToken.class);
            if(userLoginToken.required()){
                //執行認證
                if(token==null){
                    throw new RuntimeException("無token,請從新登陸");
                }else {
                    //獲取token中的用戶信息
                    Claims claims;
                    try{
                        claims= tokenUtil.parseJWT(token);

                    }catch (ExpiredJwtException e){
                        throw new RuntimeException("401,token失效");
                    }
                    String email=claims.getId();
                    User user=userDao.isExistUser(email);
                    if(user==null){
                        throw new RuntimeException("用戶不存在,請從新登陸");
                    }
                    httpServletRequest.setAttribute(CurrentUserConstants.CURRENT_USER,user);
                }
            }
        }
        return true;
    }
    // 請求處理以後進行調用,可是在視圖被渲染以前(Controller方法調用以後)
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }
    // 在整個請求結束以後被調用,也就是在DispatcherServlet 渲染了對應的視圖以後執行(主要是用於進行資源清理工做)
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e)throws Exception{

    }
}
View Code

 配置攔截器

package com.liao.tdoor.config;

import com.liao.tdoor.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

/**
 * 配置攔截器
 * @author 廖某某
 * @date 2019/02/18
 */
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        //攔截全部請求,判斷是否有@UserLogin註解,決定是否須要從新登陸
        registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
        argumentResolvers.add(currentUserMethodArgumentResolver());
        super.addArgumentResolvers(argumentResolvers);
    }
    @Bean
    public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver(){
        return new CurrentUserMethodArgumentResolver();
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor(){
        return new AuthenticationInterceptor();
    }
}
View Code

 自定義參數解析器解析token中包含的用戶信息。

package com.liao.tdoor.config;

import com.liao.tdoor.annotation.CurrentUser;
import com.liao.tdoor.model.User;
import com.liao.tdoor.responseMsg.CurrentUserConstants;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

/**
 * 自定義參數解析器(解析user)
 * @author 廖某某
 * @date 2019/02/18
 * 增長方法注入,將含有 @CurrentUser 註解的方法參數注入當前登陸用戶
 */
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter){
        return parameter.getParameterType().isAssignableFrom(User.class) //判斷是否能轉換成User類型
                && parameter.hasParameterAnnotation(CurrentUser.class); //是否有CurrentUser註解
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user = (User) webRequest.getAttribute(CurrentUserConstants.CURRENT_USER, RequestAttributes.SCOPE_REQUEST);
        if (user != null) {
            return user;
        }
        throw new MissingServletRequestPartException(CurrentUserConstants.CURRENT_USER);
    }
}
View Code

 這章就大概先說這麼多吧,好像記得也就這麼多了。

思想總結:用戶登陸傳遞郵箱密碼(缺點:沒有作到密碼加密傳輸)到服務端驗證,經過就返回token給前臺,前臺獲取token保存到本地客戶端。在HTML中我用的是localStorage保存,而後每次發起請求會根據是否須要登陸操做而向後臺傳遞token。服務端根據請求頭的token,進行用戶驗證,驗證經過就放行,不經過就返回登陸失敗信息返回前臺,前臺根據服務端返回的消息做出相應處理。

下一章說一下關於驗證經過放行操做。若是這章有什麼問題請你們見諒,並留言指正,O(∩_∩)O哈哈~。

相關文章
相關標籤/搜索