今天來寫一下怎麼登陸和維持登陸狀態。前端
相信登陸驗證你們都比較熟悉,在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")); } }
而後是用戶登陸驗證。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); }
用戶登陸成功後生成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; } }
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; } }
登陸操做完成後,客戶端根據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{ } }
配置攔截器
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(); } }
自定義參數解析器解析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); } }
這章就大概先說這麼多吧,好像記得也就這麼多了。
思想總結:用戶登陸傳遞郵箱密碼(缺點:沒有作到密碼加密傳輸)到服務端驗證,經過就返回token給前臺,前臺獲取token保存到本地客戶端。在HTML中我用的是localStorage保存,而後每次發起請求會根據是否須要登陸操做而向後臺傳遞token。服務端根據請求頭的token,進行用戶驗證,驗證經過就放行,不經過就返回登陸失敗信息返回前臺,前臺根據服務端返回的消息做出相應處理。
下一章說一下關於驗證經過放行操做。若是這章有什麼問題請你們見諒,並留言指正,O(∩_∩)O哈哈~。