<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot+SpringSecurity+Redis+jwt實現(xiàn)前后端分離攜帶驗證碼(實戰(zhàn))

          共 18007字,需瀏覽 37分鐘

           ·

          2022-01-13 16:35

          寫在前面的話

          • 小白記錄第一次整合Springboot+Springsecurity+hutool+redis+jwt。

          • 本文章使用了大量hutool的工具類,請悉知。

          • 本文章中心是攜帶驗證碼+賬號密碼請求后端驗證,使用Redis存儲驗證碼。

          • 默認(rèn)在閱讀本篇文章的朋友們對Springboot、SpringSecurity、redis、jwt已有認(rèn)知。

          • 因為本項目屬于本人的一個練手項目,所以包含的一些pom依賴如不需要請自行剝離,僅需SpringSecurity、redis、jwt即可。

          • 如有疑問請評論區(qū)友好交流指點。

          后續(xù)1(2021-12-05)

          在寫本項目的時候,我發(fā)現(xiàn)退出登錄后,雖然SpringSecurity已經(jīng)刪除了憑證,但是jwt還是可以憑借token訪問,這顯然是不太完美的。結(jié)合百度,我想到了使用redis來存儲token,實現(xiàn)退出登錄后及時刪除token。請滑到文檔底部查看最新改動代碼。

          后續(xù)2(2021-12-07)

          看到評論區(qū)朋友問有沒有g(shù)it地址,剛才上線脫敏了下配置,這就把地址開源出來了。
          后端:e.coding.net/yueranzs/vu…
          這個練手項目叫做新冠物資管理系統(tǒng),b站看了視頻,結(jié)合自己技術(shù)把前端后端都做了大變動。
          前端:e.coding.net/yueranzs/vu…

          后端目前圖片存儲使用到的技術(shù)是阿里云OSS(自封裝OSSUtil),大家可以自行復(fù)制。有疑問或補充請評論區(qū)友好交流。
          因為本身自己是個后端程序猿所以前端很多注釋,應(yīng)該是易懂的。

          orz,不過都還沒有做完,正在一步步學(xué)習(xí)中,前端架構(gòu)是拜托了一個大佬教我學(xué)習(xí)更改的,更容易擴展,大家如想學(xué)習(xí),可以down下來運行試試看。前端我可能沒做脫敏數(shù)據(jù),目前也在學(xué)習(xí)怎么把vue部署在自己的群暉NAS虛擬機上,最終結(jié)果出來我將直接公開域名,供大家訪問。

          引入相關(guān)依賴


          1.8
          UTF-8
          UTF-8
          2.3.7.RELEASE
          1.2.4
          5.7.16
          5.1.0
          3.4.1
          2.2
          0.11.2




          org.springframework.boot
          spring-boot-starter



          org.springframework.boot
          spring-boot-devtools
          runtime
          true


          mysql
          mysql-connector-java
          runtime


          org.springframework.boot
          spring-boot-configuration-processor
          true


          org.projectlombok
          lombok
          true


          org.springframework.boot
          spring-boot-starter-test
          test


          org.junit.vintage
          junit-vintage-engine






          org.springframework.boot
          spring-boot-starter-web




          org.springframework.boot
          spring-boot-starter-validation



          org.springframework.boot
          spring-boot-starter-aop





          com.alibaba
          druid-spring-boot-starter
          ${druid.version}





          cn.hutool
          hutool-all
          ${hutool.version}



          org.apache.poi
          poi
          ${poi.version}


          org.apache.poi
          poi-ooxml
          ${poi.version}




          com.baomidou
          mybatis-plus-boot-starter
          ${mybatis-plus.version}



          com.baomidou
          mybatis-plus-generator
          ${mybatis-plus.version}



          org.apache.velocity
          velocity-engine-core
          ${mybatis-plus-velocity.version}




          org.springframework.boot
          spring-boot-starter-data-redis




          org.springframework.boot
          spring-boot-starter-security




          io.jsonwebtoken
          jjwt-api
          ${jwt.version}


          io.jsonwebtoken
          jjwt-impl
          ${jwt.version}
          runtime


          io.jsonwebtoken
          jjwt-jackson
          ${jwt.version}
          runtime



          復(fù)制代碼

          驗證碼存在redis中,并返回base64到前端

          • 本文章使用的是redis存儲驗證碼,具體組成為key:code_xxx,value:英文數(shù)字(四位)

          • 驗證碼圖返回的base64使用的是hutool工具類(強烈推薦)

          LoginController

          //randomCode是一個時間戳,由前端生成后請求后端,具體是防止redis中的key重復(fù)
          @ApiOperation(value = "驗證碼",notes = "獲取驗證碼")
          @GetMapping("/getRandomCode")
          public Result getRandomCode(@RequestParam String randomCode){
          if (ObjectUtil.isEmpty(randomCode)) {
          return Result.error(500,"請輸入驗證碼!");
          }
          return Result.successData(loginService.getRandomCode(randomCode));
          }
          復(fù)制代碼

          LoginServiceImpl

          /**
          * 獲取驗證碼Base64
          *
          * @param randomCode
          * @return
          */
          @Transactional(propagation = Propagation.SUPPORTS,rollbackFor = Exception.class)
          @Override
          public String getRandomCode(String randomCode) {
          //定義圖形驗證碼的長、寬、驗證碼字符數(shù)、干擾元素個數(shù)
          ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(90, 34, 4, 3);
          //設(shè)置背景顏色
          captcha.setBackground(Color.WHITE);
          //驗證圖形驗證碼的有效性,返回boolean值
          captcha.verify("60");
          //將字符長存入redis,并判斷redis中是否存在
          //RedisUtil,我一會貼在下面
          //TimeUnit是個枚舉類,我這里選擇是以秒計時,如60秒后過期清除當(dāng)前驗證碼
          boolean redisCode = RedisUtil.set("code_" + randomCode, captcha.getCode(), 過期時長, TimeUnit.SECONDS);
          //如果存入redis中失敗,拋出異常
          //這里是自定義異常類,可以自行處理,不影響
          if (!redisCode) {
          new BusinessException(狀態(tài)碼, 返回提示信息);
          }
          //3.這里只返回Base64字符串用來展示
          return captcha.getImageBase64Data();
          }
          復(fù)制代碼

          RedisUtil工具類

          /**
          * 掘金里無法導(dǎo)入整個redis工具類,我這里挑了幾個需要的方法,僅供參考
          * redis工具類
          * @author yueranzs
          * @date 2021-03-04 10:08
          */
          public class RedisUtil {
          //因為普通類無法直接使用RedisTemplate,這里用hutool中的SpringUtil來獲取bean
          //如沒引入hutool的可以百度下springboot中java普通類怎么調(diào)用mapper或service中的接口
          //關(guān)鍵注解@Component、@PostConstruct
          private static final RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate");

          /**
          * 普通緩存放入
          *
          * @param key 鍵
          * @param value 值
          * @return true成功 false失敗
          */
          public static boolean set(String key, Object value) {
          try {
          redisTemplate.opsForValue().set(key, value);
          return true;
          } catch (Exception e) {
          e.printStackTrace();
          return false;
          }

          }

          /**
          * 刪除緩存
          *
          * @param key 可以傳一個值 或多個
          */
          @SuppressWarnings("unchecked")
          public static void del(String... key) {
          if (key != null && key.length > 0) {
          if (key.length == 1) {
          redisTemplate.delete(key[0]);
          } else {
          redisTemplate.delete(CollectionUtils.arrayToList(key));
          }
          }
          }
          }
          復(fù)制代碼

          異常類

          /**
          * @author yueranzs
          * @date 2021-11-03 17:55
          */
          @Data
          @AllArgsConstructor
          @NoArgsConstructor
          public class BusinessException extends RuntimeException{

          @ApiModelProperty(value = "狀態(tài)碼")
          private Integer code;

          @ApiModelProperty(value = "錯誤信息")
          private String errMsg;

          }
          復(fù)制代碼

          全局異常處理

          /**
          * 全局異常處理
          * @author yueranzs
          * @date 2021-11-01 11:55
          */
          @Slf4j
          @ControllerAdvice
          public class GlobalExceptionHandler {


          /**
          * 這里的意思是,只要捕獲到BusinessException異常,那么就執(zhí)行此方法
          */
          @ExceptionHandler(BusinessException.class)
          @ResponseBody
          public Result error(BusinessException exception){
          log.error(exception.getErrMsg());
          return Result.error(exception.getCode(), exception.getErrMsg());
          }



          }
          復(fù)制代碼

          封裝返回類

          /**
          * 封裝返回類
          * @author yueranzs
          * @date 2021-11-01 10:51
          */
          @Data
          public class Result {

          @ApiModelProperty(value = "是否成功")
          private Boolean success;

          @ApiModelProperty(value = "響應(yīng)碼")
          private Integer code;

          @ApiModelProperty(value = "提示信息")
          private String message;

          @ApiModelProperty(value = "返回數(shù)據(jù)")
          private Object data;

          /**
          * 構(gòu)造方法私有化,里面的方法都是靜態(tài)方法
          * 達(dá)到保護屬性的作用
          */
          private Result(){

          }

          /**
          * 這里使用鏈?zhǔn)骄幊?br> * @return
          */
          public static Result ok(){
          Result result = new Result();
          result.setSuccess(true);
          result.setCode(ResultCode.SUCCESS.getCode());
          result.setMessage(ResultCode.SUCCESS.getMessage());
          return result;
          }
          public static Result ok(Integer code,String message){
          Result result = new Result();
          result.setSuccess(true);
          result.setCode(code);
          result.setMessage(message);
          return result;
          }
          public static Result error(){
          Result result = new Result();
          result.setSuccess(false);
          //失敗code
          result.setCode(ResultCode.COMMON_FAIL.getCode());
          //失敗message
          result.setMessage(ResultCode.COMMON_FAIL.getMessage());
          return result;
          }
          public static Result error(Integer code,String message){
          Result result = new Result();
          result.setSuccess(false);
          result.setCode(code);
          result.setMessage(message);
          return result;
          }
          public static Result successData(Object data){
          Result result = new Result();
          result.setSuccess(true);
          //成功code
          result.setCode(ResultCode.SUCCESS.getCode());
          //成功message
          result.setMessage(ResultCode.SUCCESS.getMessage());
          result.setData(data);
          return result;
          }
          public static Result errorData(Object data){
          Result result = new Result();
          result.setSuccess(false);
          //失敗code
          result.setCode(ResultCode.COMMON_FAIL.getCode());
          //失敗message
          result.setMessage(ResultCode.COMMON_FAIL.getMessage());
          result.setData(data);
          return result;
          }

          /**
          * 自定義
          * @param success
          * @return
          */
          public Result success(Boolean success){
          this.setSuccess(success);
          return this;
          }
          public Result message(String message){
          this.setMessage(message);
          return this;
          }
          public Result code(Integer code){
          this.setCode(code);
          return this;
          }
          public Result data(Object data){
          this.setData(data);
          return this;
          }

          }
          復(fù)制代碼

          訪問獲取驗證碼接口

          postman請求返回的數(shù)據(jù)和結(jié)構(gòu)

          前端頁面展示情況

          redis中存儲情況

          編寫SecurityConfig配置類

          關(guān)于EnableGlobalMethodSecurity

          • 當(dāng)我們想要開啟spring方法級安全時,只需要在任何 @Configuration 實例上使用 @EnableGlobalMethodSecurity 注解就能達(dá)到此目的。同時這個注解為我們提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三種不同的機制來實現(xiàn)同一種功能。

          • 具體請訪問鏈接,有詳細(xì)解釋:blog.csdn.net/chihaihai/a…

          /**
          * @author yueranzs
          * @date 2021/11/22 13:56
          */
          @Configuration
          //開啟springsecurity
          @EnableWebSecurity
          @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
          public class SecurityConfig extends WebSecurityConfigurerAdapter {

          @Autowired
          private LoginAuthenticationProvider loginAuthenticationProvider;
          @Autowired
          private LoginUserDetails loginUserDetails;
          @Autowired
          private AuthenticationDetailsSource authenticationDetailsSource;

          /**
          * SpringSecurity5.X要求必須指定密碼加密方式,否則會在請求認(rèn)證的時候報錯
          * 同樣的,如果指定了加密方式,就必須您的密碼在數(shù)據(jù)庫中存儲的是加密后的,才能比對成功
          * @return
          */
          @Bean
          protected BCryptPasswordEncoder passwordEncoder(){
          return new BCryptPasswordEncoder();
          }

          /**
          * 注入自定義jwttoken過濾器
          */
          @Bean
          protected JwtAuthenticationTokenFilter authenticationTokenFilter() throws Exception{
          return new JwtAuthenticationTokenFilter();
          }

          /**
          * 角色繼承,比如在一個系統(tǒng)中admin屬于最高角色"超級管理員",那么他將擁有其他角色所有的權(quán)限
          * 以>來設(shè)置
          * admin > user > normal > ......
          * @return
          */
          @Bean
          RoleHierarchy roleHierarchy(){
          RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
          hierarchy.setHierarchy("ROLE_admin > ROLE_user");
          return hierarchy;
          }

          /**
          * 靜態(tài)資源放行
          */
          @Override
          public void configure(WebSecurity web) {
          web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
          }

          /**
          * Springsecurity默認(rèn)不攜帶驗證碼進(jìn)行驗證,所以這里我們需要重寫相關(guān)配置類,一會請看代碼
          */
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
          //將自定義的Provider裝配到Builder
          auth.authenticationProvider(loginAuthenticationProvider);
          //將自定義的loginserviceimpl裝配到builder
          auth.userDetailsService(loginUserDetails).passwordEncoder(new PasswordEncoder() {
          @Override
          public String encode(CharSequence rawPassword) {
          return rawPassword.toString();
          }

          @Override
          public boolean matches(CharSequence rawPassword, String encodedPassword) {
          return encodedPassword.equals(rawPassword.toString());
          }
          });
          }

          @Override
          protected void configure(HttpSecurity http) throws Exception {
          //Springsecurity放行規(guī)則,permitAll是針對所有方法。
          //目使用了swagger,所以需要將swagger相關(guān)的url放行。
          //SpringseCurity的放行規(guī)則由上往下,如果前者已被攔截,
          //不再執(zhí)行,所以這就是為什么.anyRequest().authenticated()需要放在最后的原因。
          http.authorizeRequests()
          .antMatchers("/webjars/**").permitAll()
          .antMatchers("/swagger-resources/**").permitAll()
          .antMatchers("/v2/*").permitAll()
          .antMatchers("/login/**").permitAll()
          //剩下方法攔截
          .anyRequest().authenticated()
          .and()
          .formLogin()
          //登錄頁
          .loginPage("/login.html")
          //登錄請求接口,如果url為空也會默認(rèn)將loginPage的值賦值給url
          //可能習(xí)慣性會認(rèn)為需要自己寫一個/login/loginUser的接口,
          //但其實這里是交給SpringSecurity自己去檢驗的,默認(rèn)情況下只需要攜帶form-data類型的賬號密碼提交即可。
          //本項目將會對默認(rèn)請求進(jìn)行重寫,使用存在redis中的驗證碼驗證
          .loginProcessingUrl("/login/loginUser")
          //設(shè)置登錄參數(shù)別名
          //SpringSecurity默認(rèn)情況下賬號和密碼的屬性名為username、password。
          //當(dāng)然也可以跟我一樣重新設(shè)置別名。(雖然設(shè)置的是一樣的,orz)
          .usernameParameter("username")
          .passwordParameter("password")
          //登錄成功后的回調(diào),我看其他博客寫的是自定義返回類,因為我并沒做其他操作,就簡單一點吧,看后面代碼。
          //為什么是HttpResponseResult::loginSuccess而其他的卻是->?
          //因為登錄成功接口我的形參和該方法形參一致,所以可以這樣寫
          .successHandler(HttpResponseResult::loginSuccess)
          //登錄失敗回調(diào)
          .failureHandler((req, resp, e) -> HttpResponseResult.loginError(resp,e))
          //權(quán)限不足回調(diào)
          .accessDeniedHandler(HttpResponseResult::insufficientPermissions)
          //自定義authenticationDetailsSource,目的是為了獲取請求的驗證碼等信息
          .authenticationDetailsSource(authenticationDetailsSource)
          .permitAll()
          .and()
          .csrf().disable()
          .exceptionHandling()
          .authenticationEntryPoint((req, resp, auth) -> HttpResponseResult.noLogin(resp))
          .and()
          //設(shè)置無狀態(tài)的連接,即不創(chuàng)建session
          .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
          //退出登錄
          .logout()
          .logoutUrl("/login/logout")
          .logoutSuccessHandler((req,resp,auth) ->HttpResponseResult.logout(resp))
          .permitAll()
          .and()
          ;

          //使用自定義的jwttoken過濾器來進(jìn)行驗證
          http.addFilterBefore(authenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
          //禁止頁面緩存
          http.headers().cacheControl();
          }

          }
          復(fù)制代碼

          編寫SpringSecurity的回調(diào)返回類

          /**
          * 針對返回響應(yīng)的封裝
          * @author yueranzs
          * @date 2021/11/22 14:13
          */
          @Data
          public class HttpResponseResult {


          /**
          * 基礎(chǔ)返回
          * @param resp
          * @param jsonObject
          * @throws IOException
          */
          public static void base(HttpServletResponse resp,JSONObject jsonObject) throws IOException {
          resp.setContentType("application/json;charset=utf-8");
          PrintWriter out = resp.getWriter();
          out.println(jsonObject);
          out.flush();
          out.close();
          }
          /**
          * 響應(yīng)返回封裝
          * @param resp
          * @param resultCode
          * @return
          */
          public static void data(HttpServletResponse resp,CustomizeResultCode resultCode) throws IOException {
          JSONObject result = new JSONObject();
          result.set("code",resultCode.getCode());
          result.set("message",resultCode.getMessage());
          base(resp,result);
          }

          /**
          * 暫無憑證或是認(rèn)證失敗
          * @param resp
          */
          public static void noProof(HttpServletResponse resp) throws IOException {
          data(resp,UserResultCode.USER_NOT_PROOF);
          }

          /**
          * 登錄失敗
          * @param resp
          * @param exception security的認(rèn)證異常
          */
          public static void loginError(HttpServletResponse resp,AuthenticationException exception) throws IOException {
          if (exception instanceof LockedException) {
          //賬戶鎖定
          data(resp,UserResultCode.USER_ACCOUNT_LOCKED);
          } else if (exception instanceof CredentialsExpiredException) {
          //密碼過期
          data(resp,UserResultCode.USER_CREDENTIALS_EXPIRED);
          } else if (exception instanceof AccountExpiredException) {
          //賬戶過期
          data(resp,UserResultCode.USER_ACCOUNT_EXPIRED);
          } else if (exception instanceof DisabledException) {
          //賬戶禁用
          data(resp,UserResultCode.USER_ACCOUNT_DISABLE);
          } else if (exception instanceof BadCredentialsException) {
          //用戶名或者密碼輸入錯誤
          data(resp,UserResultCode.USER_LOGIN_ERROR_NO);
          }else if (exception instanceof InternalAuthenticationServiceException){
          //用戶不存在
          data(resp,UserResultCode.USER_ACCOUNT_NOT_EXIST);
          }
          }

          /**
          * 退出
          * @param resp
          */
          public static void logout(HttpServletResponse resp) throws IOException {
          data(resp,UserResultCode.USER_LOGOUT_SUCCESS);
          }

          /**
          * 登錄成功
          * @param req
          * @param resp
          * @param auth
          */
          public static void loginSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
          //生成token
          JwtUtil jwtUtil = new JwtUtil();
          Map user = new HashMap<>();
          user.put("username",auth.getName());
          //token我只包含了username,因為在下面自定義jwttoken過濾器里面會查詢角色等信息
          String token = jwtUtil.create(user);
          base(resp,new JSONObject().set("code",200).set("data",token));
          }

          /**
          * 權(quán)限不足
          * @param req
          * @param resp
          * @param e
          */
          public static void insufficientPermissions(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException {
          data(resp,UserResultCode.USER_INSUFFICIENT_PERMISSIONS);
          }
          }
          復(fù)制代碼

          User類(pojo)

          注意,下面用戶表是mybatis-plus生成,如果想通過SpringSecurity驗證,需要實現(xiàn)UserDetails

          /**
          *


          * 用戶表
          *


          *
          * @author yueranzs
          * @since 2021-11-04
          */
          @Data
          @EqualsAndHashCode(callSuper = false)
          @TableName("tb_user")
          @ApiModel(value="User對象", description="用戶表")
          public class User implements Serializable, UserDetails {

          private static final long serialVersionUID = 1L;

          @ApiModelProperty(value = "用戶ID")
          @TableId(value = "id", type = IdType.AUTO)
          private Long id;

          @ApiModelProperty(value = "用戶名")
          private String username;

          @ApiModelProperty(value = "昵稱")
          private String nickname;

          @ApiModelProperty(value = "郵箱")
          private String email;

          @ApiModelProperty(value = "頭像")
          private String avatar;
          @ApiModelProperty(value = "頭像臨時簽名")
          @TableField(exist = false)
          private String avatarUrl;

          @ApiModelProperty(value = "聯(lián)系電話")
          private String phoneNumber;

          @ApiModelProperty(value = "狀態(tài) 0鎖定 1有效")
          private Integer status;

          @ApiModelProperty(value = "創(chuàng)建時間")
          private Date createTime;

          @ApiModelProperty(value = "修改時間")
          private Date modifiedTime;

          @ApiModelProperty(value = "性別 0男 1女 2保密")
          private Integer sex;

          @ApiModelProperty(value = "鹽")
          private String salt;

          @ApiModelProperty(value = "0:超級管理員,1:系統(tǒng)用戶")
          private Integer type;

          @ApiModelProperty(value = "密碼")
          private String password;

          @ApiModelProperty(value = "生日")
          private Date birth;

          @ApiModelProperty(value = "部門id")
          private Long departmentId;

          @ApiModelProperty(value = "邏輯刪除")
          private Integer deleted;

          @ApiModelProperty(value = "角色信息")
          //mybatis-plus中的注解,即在對數(shù)據(jù)庫操作時忽略本字段
          @TableField(exist = false)
          private Set authorities;

          @Override
          public boolean isAccountNonExpired() {
          return true;
          }

          @Override
          public boolean isAccountNonLocked() {
          return true;
          }

          @Override
          public boolean isCredentialsNonExpired() {
          return true;
          }

          @Override
          public boolean isEnabled() {
          return true;
          }
          }
          復(fù)制代碼

          編寫LoginAuthenticationDetailsSource類

          /**
          * 描述:自定義AuthenticationDetailsSource,將HttpServletRequest注入到AuthenticationDetails,使其能獲取到請求中的驗證碼等其他信息
          * @author yueranzs
          * @date 2021/12/1 9:42
          */
          @Component
          public class LoginAuthenticationDetailsSource implements AuthenticationDetailsSource {
          @Override
          public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
          return new LoginWebAuthenticationDetails(request);
          }
          }
          復(fù)制代碼

          編寫LoginWebAuthenticationDetails類

          /**
          * 描述:自定義WebAuthenticationDetails,將驗證碼和用戶名、密碼一同帶入AuthenticationProvider中
          * @author yueranzs
          * @date 2021/12/1 9:38
          */
          public class LoginWebAuthenticationDetails extends WebAuthenticationDetails {
          private static final long serialVersionUID = 6975601077710753878L;
          /*驗證碼value*/
          private final String code;
          /*驗證碼key*/
          private final String randomCode;
          public LoginWebAuthenticationDetails(HttpServletRequest request) {
          super(request);
          //這里的code是指驗證碼真實code,即redis中的驗證碼value,可自行修改成自己項目的屬性名
          code = request.getParameter("code");
          //redis中的驗證碼key,可自行修改成自己項目的屬性名
          randomCode = request.getParameter("randomCode");
          }

          public String getRandomCode() {
          return randomCode;
          }
          public String getCode() {
          return code;
          }

          @Override
          public String toString() {
          StringBuilder sb = new StringBuilder();
          sb.append(super.toString()).append("; code: ").append(this.getCode());
          sb.append(super.toString()).append("; randomCode: ").append(this.getRandomCode());
          return sb.toString();
          }
          }
          復(fù)制代碼

          編寫LoginUserDetails類

          /**
          * @author yueranzs
          * @date 2021/12/1 11:38
          */
          @Component
          public class LoginUserDetails implements UserDetailsService {

          @Autowired
          private UserService userService;

          /**
          * 這里是根據(jù)username(賬號)去查詢數(shù)據(jù)庫,然后進(jìn)行檢驗
          */
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          //mybatis-plus的語句,意思是查詢單個的用戶根據(jù)用戶名(username)和偽刪除(delflag)來查
          User user = userService.getOne(new QueryWrapper().lambda().select(User::getId,User::getUsername, User::getPassword)
          .eq(User::getUsername, username)
          .eq(User::getDeleted, ResultCode.NODELETE.getCode()));
          if (ObjectUtil.isNull(user)) {
          //用戶不存在,拋出SpringSecurity異常
          throw new InternalAuthenticationServiceException(UserResultCode.USER_ACCOUNT_NOT_EXIST.getMessage());
          }
          //查詢角色
          List roles = userService.getRolesByUserId(user.getId());
          Set authorities = new HashSet();
          //注意:SpringSecurity授權(quán)分兩種:角色和權(quán)限
          //角色授權(quán):在授權(quán)時,前綴必須加上"ROLE_",一般使用AuthorityUtils.commaSeparatedStringToAuthorityList(字符串,用逗號添加多個role)
          //AuthorityUtils.commaSeparatedStringToAuthorityList就不需要自己加"ROLE_"了
          //權(quán)限授權(quán):不需要加前綴

          //后面的hasRole和hasAuthority千萬不要搞錯了,Role是角色,Authority是權(quán)限,我當(dāng)初就是看錯了,找了很久的問題,后面看代碼
          roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName())));

          //千萬要記得查詢到角色信息后記得設(shè)置
          user.setAuthorities(authorities);

          //返回
          return user;
          }
          }
          復(fù)制代碼

          編寫LoginAuthenticationProvider類

          /**
          * 描述:自定義SpringSecurity的認(rèn)證器
          * @author yueranzs
          * @date 2021/12/1 9:44
          */
          @Component
          public class LoginAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

          @Autowired
          private LoginUserDetails loginUserDetails;

          @Override
          protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

          }

          @Override
          protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
          return null;
          }

          @Override
          public Authentication authenticate(Authentication authentication) throws AuthenticationException {
          //用戶名
          String username = authentication.getName();
          //密碼
          String password = authentication.getCredentials().toString();
          LoginWebAuthenticationDetails loginWebAuthenticationDetails= (LoginWebAuthenticationDetails)authentication.getDetails();
          //驗證碼key
          String randomCode = loginWebAuthenticationDetails.getRandomCode();
          //驗證碼value
          String code = loginWebAuthenticationDetails.getCode();
          //驗證碼是否為空
          if (ObjectUtil.isEmpty(randomCode) || ObjectUtil.isEmpty(code)) {
          throw new NullPointerException("請輸入驗證碼");
          }
          //檢驗驗證碼是否正確
          if (!validateVerifyRandomCode(randomCode,code)) {
          throw new BusinessException(UserResultCode.REDIS_CODE.getCode(), UserResultCode.REDIS_CODE.getMessage());
          }
          User user = (User) loginUserDetails.loadUserByUsername(username);
          //密碼是否一致
          if (!user.getPassword().equals(SecureUtil.md5(password))) {
          //密碼錯誤,不過因為安全性的問題所以返回此異常,意思是用戶名或者密碼錯誤
          throw new BadCredentialsException(UserResultCode.USER_CREDENTIALS_ERROR.getMessage());
          }
          //刪除redis的驗證碼
          RedisUtil.del("code_" + randomCode);

          return this.createSuccessAuthentication(user,authentication,user);
          }

          /**
          * 驗證用戶輸入的驗證碼
          * @param randomCode 驗證碼key
          * @param code 驗證碼value
          * @return
          */
          public boolean validateVerifyRandomCode(String randomCode,String code){
          //驗證碼是否一致
          Object redisCode = RedisUtil.get("code_" + randomCode);
          return ObjectUtil.equals(code, redisCode);
          }
          }
          復(fù)制代碼

          編寫Jwt配置類

          /**
          * jwt配置類
          * @author yueranzs
          * @date 2021/12/4 9:57
          */
          @Data
          @ToString
          @Configuration
          //與配置文件中的數(shù)據(jù)關(guān)聯(lián)起來(這個注解會默認(rèn)自動匹配jwt開頭的配置)
          @ConfigurationProperties(prefix = "jwt")
          public class JwtConfig {

          /*request Headers : Authorization*/
          private String header;

          /*Base64對該令牌進(jìn)行編碼*/
          private String base64Secret;

          /*令牌過期時間 此處單位/毫秒 */
          private Long tokenValidityInSeconds;

          }
          復(fù)制代碼

          JwtUtil工具類

          注意,本工具類建立在hutool工具類的基礎(chǔ)上,僅供參考,部分屬性值請視自己情況定

          /**
          * jwt工具類
          * @author yueranzs
          * @date 2021/11/25 15:55
          */
          @Component
          public class JwtUtil {

          private static JwtConfig jwtConfig;

          @Autowired
          private void setJwtConfig(JwtConfig jwtConfig){
          JwtUtil.jwtConfig = jwtConfig;
          }

          /**
          * 生成jwt
          * @param payload 數(shù)據(jù)主體
          * @return
          */
          public String create(Map payload){
          //每個jwt都默認(rèn)生成一個到期時間
          payload.put("expire_time", DateUtil.current() + jwtConfig.getTokenValidityInSeconds());
          //生成私鑰
          JWTSigner jwtSigner = JWTSignerUtil.hs256(jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));
          //生成token
          return JWTUtil.createToken(payload,jwtSigner);
          }

          /**
          * 解析jwt
          * @param token
          * @return
          */
          public JSONObject parse(String token){
          return JWTUtil.parseToken(token).getPayload().getClaimsJson();
          }

          /**
          * 校驗token是否正確
          * @param token
          * @return
          */
          public boolean verifyToken(String token){
          //先判斷是否到期,再判斷是否正確
          if (expiredToken(token)) {
          return JWTUtil.verify(token,jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));
          }
          return false;
          }

          /**
          * 校驗token是否過期
          * @param token
          * @return
          */
          public boolean expiredToken(String token){
          return DateUtil.current() < getExpiredToken(token);
          }

          /**
          * 獲取token過期時間
          * @param token
          * @return
          */
          public long getExpiredToken(String token){
          return Long.parseLong(parse(token).get("expire_time").toString());
          }

          /**
          * 獲取登錄人賬號
          * @param token
          * @return
          */
          public String getUserNameToken(String token){
          return parse(token).get("username").toString();
          }

          /**
          * 獲取登錄人角色集合
          * @param token
          * @return
          */
          public Set getRolesToken(String token){
          return (Set) parse(token).get("authorities");
          }

          }
          復(fù)制代碼

          applicaiton.yml中進(jìn)行追加jwt信息

          jwt:
          # 請求頭,就是在header中攜帶的令牌名稱,任意名字都可以
          header: Authorization
          # 鹽值
          base64-secret: jwt加密的密鑰,任意填寫
          # 過期時間 ,單位/毫秒
          token-validity-in-seconds: 過期時間
          復(fù)制代碼

          編寫JwtAuthenticationTokenFilter過濾類

          /**
          * jwttokenfilter
          * @author yueranzs
          * @date 2021/12/4 10:14
          */
          @Slf4j
          @Component
          public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

          @Resource
          private UserDetailsService userDetailsService;

          @Resource
          private JwtUtil jwtUtil;

          @Resource
          private JwtConfig jwtConfig;

          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          String requestUrl = request.getRequestURI();
          String authToken = request.getHeader(jwtConfig.getHeader());
          String userName = null;
          if (ObjectUtil.isNotEmpty(authToken)) {
          userName = jwtUtil.getUserNameToken(authToken);
          }

          log.info("進(jìn)入jwt自定義token過濾器");
          log.info("自定義token過濾器獲得用戶名為:" + userName);

          //當(dāng)userName不為空時進(jìn)行校驗token是否為有效token
          //ObjectUtil.isNotEmpty()和ObjectUtil.isNull()是hutool中的方法。
          /*
          前者意思是指對象是否不為空,和isNotNull()不同。
          比如"",isNotNull()會返回true而isNotEmpty()會返回false。
          userName是字符串所以使用isNotEmpty(),該方法也很適合集合判空
          */
          /*
          getAuthentication()使用isNull()原因是:
          通過前面幾個代碼塊的代碼,可以看出是存儲授權(quán)信息的
          這里的意思是如果用戶名不為空并且授權(quán)信息又有值,那么就直接跳過,反之就是進(jìn)入下面的if內(nèi)部
          */
          if (ObjectUtil.isNotEmpty(userName) && ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())) {
          UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
          User user = (User) userDetails;
          //檢驗token
          if (!jwtUtil.verifyToken(authToken)) {
          throw new BusinessException(500,"token已過期");
          }else if (StrUtil.equals(userName,user.getUsername())){
          /**
          * UsernamePasswordAuthenticationToken繼承AbstractAuthenticationToken實現(xiàn)Authentication
          * 所以當(dāng)在頁面中輸入用戶名和密碼之后首先會進(jìn)入到UsernamePasswordAuthenticationToken驗證(Authentication),
          * 然后生成的Authentication會被交由AuthenticationManager來進(jìn)行管理
          * 而AuthenticationManager管理一系列的AuthenticationProvider,
          * 而每一個Provider都會通UserDetailsService和UserDetail來返回一個
          * 以UsernamePasswordAuthenticationToken實現(xiàn)的帶用戶名和密碼以及權(quán)限的Authentication
          */
          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
          //將authentication放入SecurityContextHolder中
          SecurityContextHolder.getContext().setAuthentication(authentication);
          }
          }
          filterChain.doFilter(request,response);
          }
          }
          復(fù)制代碼

          hasRole、hasAuthority

          關(guān)于這兩只的區(qū)別可以看鏈接:Spring Security 中的 hasRole 和 hasAuthority 有區(qū)別嗎? - 云+社區(qū) - 騰訊云 (tencent.com)

          /**
          * 前面代碼塊中我說過這兩個注解千萬不要混淆,雖然在使用上,都并不需要加前綴
          * 但我之前沒注意清楚,在給用戶授權(quán)時我寫了ROLE_admin,但是使用的是hasAuthority
          * 也就導(dǎo)致我怎么都訪問不了這個方法,后面半信半疑hasAuthority('ROLE_admin')才能訪問
          * 再后來發(fā)現(xiàn)是自己用錯方法了,換上hasRole('admin')就沒問題
          *
          * @PreAuthorize可以看我第一個分享的鏈接
          * hashRole和hasAuthority在springsecurity4的時候才有了ROLE_前綴區(qū)分,早期幾乎是一模一樣的
          * @return
          */
          @PreAuthorize("hasAuthority('admin')")
          @ApiOperation(value = "測試一下",notes = "測試一下")
          @GetMapping("/test")
          public Result test(){
          return Result.successData("hahah");
          }
          復(fù)制代碼

          運行效果

          登錄成功

          登錄失敗

          token過期

          暫無權(quán)限

          ps:其他的一些狀態(tài)碼暫未測試,目前這些也已足以,后續(xù)如有其他需要補充的我會再來添代碼。就先這樣吧。謝謝閱讀。

          后續(xù)1-改造代碼(2021-12-05)

          調(diào)整SpringSecurity的回調(diào)返回類中l(wèi)oginSuccess()

          //加上下面注解,因為需要獲取jwtConfig的過期時間
          @Component
          public class HttpResponseResult {

          //關(guān)于jwtConfig的都是需要新增的代碼
          private static JwtConfig jwtConfig;

          @Autowired
          private void setJwtConfig(JwtConfig jwtConfig){
          HttpResponseResult.jwtConfig = jwtConfig;
          }

          /**
          * 登錄成功
          * @param req
          * @param resp
          * @param auth
          */
          public static void loginSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
          //生成token
          JwtUtil jwtUtil = new JwtUtil();
          Map user = new HashMap<>();
          user.put("username",auth.getName());
          String token = jwtUtil.create(user);

          //將token存入redis
          //這里的key結(jié)構(gòu)為:"token_" + userName
          //token失效時間和jwt失效時間保持一致
          //jwtConfig.getTokenValidityInSeconds():獲取jwt失效時間
          RedisUtil.set("token_" + auth.getName(), token, jwtConfig.getTokenValidityInSeconds(), TimeUnit.SECONDS);

          base(resp,new JSONObject().set("code",200).set("data",token));
          }

          }
          復(fù)制代碼

          調(diào)整SecurityConfig部分代碼

          將關(guān)于退出登錄的security方法全部刪除,為什么?
          本來是想在退出方法里進(jìn)行刪除redis的token,但是因為存儲token的key一部分是獲取當(dāng)前userName,而只要訪問了logout()就默認(rèn)進(jìn)入了security的過濾器,如果想改變的話會比較麻煩,所以我打算自行實現(xiàn),很方便。


          只需注釋"http://退出登錄,這里劃重點,我全部注釋了"下面代碼即可。

          @Override
          protected void configure(HttpSecurity http) throws Exception {
          http.authorizeRequests()
          .antMatchers("/webjars/**").permitAll()
          .antMatchers("/swagger-resources/**").permitAll()
          .antMatchers("/v2/*").permitAll()
          .antMatchers("/login/getRandomCode","/login/getUserAvatar").permitAll()
          // .antMatchers("/admin/**").hasRole("admin")
          // .antMatchers("/user/**").hasRole("user")
          .anyRequest().authenticated()
          .and()
          .formLogin()
          //登錄頁
          .loginPage("/login.html")
          //登錄請求接口,如果url為空也會默認(rèn)將loginPage的值賦值給url
          .loginProcessingUrl("/login/loginUser")
          //設(shè)置登錄參數(shù)別名
          .usernameParameter("username")
          .passwordParameter("password")
          .successHandler(HttpResponseResult::loginSuccess)
          .failureHandler((req, resp, e) -> HttpResponseResult.loginError(resp,e))
          .authenticationDetailsSource(authenticationDetailsSource)
          .permitAll()
          .and()
          .csrf().disable()
          .exceptionHandling()
          .authenticationEntryPoint((req, resp, auth) -> HttpResponseResult.noProof(resp))
          .accessDeniedHandler(HttpResponseResult::insufficientPermissions)
          .and()
          //設(shè)置無狀態(tài)的連接,即不創(chuàng)建session
          .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
          //退出登錄,這里劃重點,我全部注釋了
          /*.logout()
          .logoutUrl("/login/logout")
          .logoutSuccessHandler(HttpResponseResult::logout)
          .permitAll()
          .and()*/
          ;

          http.addFilterBefore(authenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
          //禁止頁面緩存
          http.headers().cacheControl();
          }
          復(fù)制代碼

          新增UserUtil工具類

          方便獲取當(dāng)前登陸人信息

          /**
          * @author yueranzs
          * @date 2021/12/5 12:24
          */
          public class UserUtil {


          /**
          * 獲取當(dāng)前登陸人信息
          * @return
          */
          public static User getUser(){
          return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
          }
          /**
          * 獲取當(dāng)前登錄人賬號
          * @return
          */
          public static String getUserName(){
          return getUser().getUsername();
          }
          /**
          * 獲取當(dāng)前登錄人編號
          * @return
          */
          public static Long getUserId(){
          return getUser().getId();
          }

          /**
          * 獲取當(dāng)前登錄人角色信息
          * @return
          */
          public static Set getUserRole(){
          return getUser().getAuthorities();
          }

          }
          復(fù)制代碼

          LoginController中新增logout()退出登錄方法

          @ApiOperation(value = "退出登錄",notes = "退出登錄")
          @GetMapping("/logout")
          public JSONObject logout(){
          return loginService.logout();
          }
          復(fù)制代碼

          LoginService實現(xiàn)類

          /**
          * 退出登錄
          *
          * @return
          */
          @Override
          public JSONObject logout() {
          //僅作返回退出登錄的結(jié)果
          JSONObject object = new JSONObject();
          //查找redis中是否存在此token,如果不為空(isNotEmpty的用法上面說過)就刪除該token
          if (ObjectUtil.isNotEmpty(RedisUtil.get("token_" + UserUtil.getUserName()))) {
          //清除token,注意順序不要弄反了
          //要先清除redis的token才能清除認(rèn)證對象,不然是無法通過UserUtil獲取到userName的
          RedisUtil.del("token_" + UserUtil.getUserName());
          //只需要清除認(rèn)證對象,因為在springsecurity中并沒有設(shè)置session,所以不需要清空
          SecurityContextHolder.getContext().setAuthentication(null);

          //退出登錄成功code
          object.set("code",UserResultCode.USER_LOGOUT_SUCCESS.getCode());

          //退出登錄成功message
          object.set("message",UserResultCode.USER_LOGOUT_SUCCESS.getMessage());
          return object;
          }
          //退出登錄失敗code
          object.set("code",UserResultCode.USER_LOGOUT_ERROR.getCode());

          //退出登錄失敗message
          object.set("message",UserResultCode.USER_LOGOUT_ERROR.getMessage());
          return object;
          }
          復(fù)制代碼

          調(diào)整JwtAuthenticationTokenFilter

          /**
          * jwttokenfilter
          * @author yueranzs
          * @date 2021/12/4 10:14
          */
          @Slf4j
          @Component
          public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

          @Resource
          private UserDetailsService userDetailsService;

          @Resource
          private JwtUtil jwtUtil;

          @Resource
          private JwtConfig jwtConfig;

          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          String requestUrl = request.getRequestURI();
          String authToken = request.getHeader(jwtConfig.getHeader());
          String userName = null;
          if (ObjectUtil.isNotEmpty(authToken)) {
          userName = jwtUtil.getUserNameToken(authToken);
          }

          log.info("進(jìn)入jwt自定義token過濾器");
          log.info("自定義token過濾器獲得用戶名為:" + userName);

          //當(dāng)userName不為空時進(jìn)行校驗token是否為有效token
          if (ObjectUtil.isNotEmpty(userName) && ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())) {
          UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
          User user = (User) userDetails;
          //檢驗token,新增下面的if判斷
          if(ObjectUtil.isEmpty(RedisUtil.get("token_" + userName))){
          throw new BusinessException(500,"token已被清除");
          } else if (!jwtUtil.verifyToken(authToken)) {
          throw new BusinessException(500,"token已過期");
          }else if (StrUtil.equals(userName,user.getUsername())) {
          /**
          * UsernamePasswordAuthenticationToken繼承AbstractAuthenticationToken實現(xiàn)Authentication
          * 所以當(dāng)在頁面中輸入用戶名和密碼之后首先會進(jìn)入到UsernamePasswordAuthenticationToken驗證(Authentication),
          * 然后生成的Authentication會被交由AuthenticationManager來進(jìn)行管理
          * 而AuthenticationManager管理一系列的AuthenticationProvider,
          * 而每一個Provider都會通UserDetailsService和UserDetail來返回一個
          * 以UsernamePasswordAuthenticationToken實現(xiàn)的帶用戶名和密碼以及權(quán)限的Authentication
          */
          //清除密碼
          user.setPassword(null);
          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
          //將authentication放入SecurityContextHolder中
          SecurityContextHolder.getContext().setAuthentication(authentication);
          }
          }
          filterChain.doFilter(request,response);
          }
          }
          復(fù)制代碼

          運行結(jié)果

          后面的話

          那么就先補充到這里吧,等后續(xù)還缺漏什么的我會繼續(xù)更新,謝謝觀看。


          作者:yueranzs
          鏈接:https://juejin.cn/post/7037792636807151630
          來源:稀土掘金
          著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。



          瀏覽 172
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美骚逼黄色片 | 免费超碰在线观看 | 欧美国产成人精品一区二区三区 | 色综合激情视频 | 天天爱天天干天天 |