整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          臺(tái)灣華視又出包!字幕變亂碼火星文 臺(tái)網(wǎng)友:螺絲掉滿地

          臺(tái)灣華視又出包!字幕變亂碼火星文 臺(tái)網(wǎng)友:螺絲掉滿地

          灣華視新聞18日晚間字幕出現(xiàn)亂碼。圖片來源:臺(tái)灣“中時(shí)新聞網(wǎng)”

          華夏經(jīng)緯網(wǎng)5月22日訊:據(jù)臺(tái)灣“中時(shí)新聞網(wǎng)”報(bào)導(dǎo),從4月20日開始,臺(tái)灣華視新聞因跑馬燈字幕問題已連出7包,沒想到21日又有網(wǎng)友抓包,在網(wǎng)絡(luò)上爆料華視新聞18日晚間字幕出現(xiàn)亂碼“張善政出戰(zhàn) 鄭運(yùn)鵬認(rèn):桃園‘藍(lán)>綠’必?cái) 保^沒幾秒后疑似內(nèi)部人員發(fā)現(xiàn)錯(cuò)誤,整個(gè)畫面的標(biāo)題全拿掉,引發(fā)網(wǎng)友熱議,甚至還有網(wǎng)友分析出這段文字出現(xiàn)的可能原因。

          短短24天內(nèi),華視新聞連環(huán)出包案,令外界感到不可思議。4月20日早上7時(shí)新聞跑馬燈誤植“新北市遭解放軍導(dǎo)彈擊中”等字幕,同日9時(shí)二度出包 “大屯火山爆發(fā) 巖漿滾滾而下”、“臺(tái)北凌晨下起拳頭大冰雹 市區(qū)交通大亂”,引發(fā)軒然大波,陳郁秀、陳雅琳為此雙雙請(qǐng)辭董事長、代總經(jīng)理職務(wù)。未料,4月24日三度出包把臺(tái)當(dāng)局行政機(jī)構(gòu)負(fù)責(zé)人蘇貞昌誤植成臺(tái)灣地區(qū)領(lǐng)導(dǎo)人。

          鄭自隆4月29日接任華視董事長,不斷說要改革,但華視仍頻頻出包,包括誤植美國職棒隊(duì)徽、快訊打出“上海日增1181萬例,確診近1個(gè)月新低”錯(cuò)誤訊息,以及將蔡英文打成“蔡EE”,以及將前臺(tái)灣地區(qū)副領(lǐng)導(dǎo)人陳建仁打成“美國副總統(tǒng)”,加上5月18日的“張善政出戰(zhàn) 鄭運(yùn)鵬認(rèn):桃園‘藍(lán)>綠’必?cái) 眮y碼文,鄭自隆任職任職不到1個(gè)月,就出5包,實(shí)在有點(diǎn)尷尬。

          此事也引發(fā)島內(nèi)網(wǎng)友熱議,“蠻好笑的”、“那是什么意思?”、“有什么密碼嗎?”、“有比蔡EE、美國副總統(tǒng)嚴(yán)重嗎”、“要不要下次也把字幕撤了”、“不要有任何字就不會(huì)錯(cuò)了”、“真是螺絲掉滿地”。

          還有些網(wǎng)友則紛紛猜起這段文字原本要表達(dá)的內(nèi)容,“這個(gè)是html的空格符嗎?”、“大于的html escape”、“空格符是 這個(gè)應(yīng)該是>大于符”、“>是html的大于的意思”、“這是原本要打什么”、“因?yàn)樽帜怀绦蛑校笥诜?hào)(>),可能為保留字符,所以鍵入時(shí)要輸 >”、“gt就是大于”。

          行展示

          后端

          主要展示 Spring Security 與 JWT 結(jié)合使用構(gòu)建后端 API 接口。

          主要功能包括登陸(如何在 Spring Security 中添加驗(yàn)證碼登陸),查找,創(chuàng)建,刪除并對(duì)用戶權(quán)限進(jìn)行區(qū)分等等。

          ps:由于只是 Demo,所以沒有調(diào)用數(shù)據(jù)庫,以上所說增刪改查均在 HashMap 中完成。

          前端

          展示如何使用 Vue 構(gòu)建前端后與后端的配合,包括跨域的設(shè)置,前端登陸攔截

          并實(shí)現(xiàn) POST,GET,DELETE 請(qǐng)求。包括如何在 Vue 中使用后端的 XSRF-TOKEN 防范 CSRF 攻擊

          技術(shù)棧


          實(shí)現(xiàn)細(xì)節(jié)

          創(chuàng)建 Spring boot 項(xiàng)目,添加 JJWT 和 Spring Security 的項(xiàng)目依賴,這個(gè)非常簡單,有很多的教程都有塊內(nèi)容,唯一需要注意的是,如果你使用的 Java 版本是 11,那么你還需要添加以下依賴,使用 Java8 則不需要。

          <dependency>
             <groupId>javax.xml.bind</groupId>
             <artifactId>jaxb-api</artifactId>
             <version>2.3.0</version>
          </dependency>

          要使用 Spring Security 實(shí)現(xiàn)對(duì)用戶的權(quán)限控制,首先需要實(shí)現(xiàn)一個(gè)簡單的 User 對(duì)象實(shí)現(xiàn) UserDetails 接口,UserDetails 接口負(fù)責(zé)提供核心用戶的信息,如果你只需要用戶登陸的賬號(hào)密碼,不需要其它信息,如驗(yàn)證碼等,那么你可以直接使用 Spring Security 默認(rèn)提供的 User 類,而不需要自己實(shí)現(xiàn)。

          public class User implements UserDetails {
              private String username;
              private String password;
              private Boolean rememberMe;
              private String verifyCode;
              private String power;
              private Long expirationTime;
              private List<GrantedAuthority> authorities;
          
              /**
              * 省略其它的 get set 方法
              */
          
              @Override
              public Collection<? extends GrantedAuthority> getAuthorities() {
                  return authorities;
              }
          
              @Override
              public String getPassword() {
                  return password;
              }
          
              @Override
              public String getUsername() {
                  return username;
              }
          
              @Override
              public boolean isAccountNonExpired() {
                  return true;
              }
          
              @Override
              public boolean isAccountNonLocked() {
                  return true;
              }
          
              @Override
              public boolean isCredentialsNonExpired() {
                  return true;
              }
          
              @Override
              public boolean isEnabled() {
                  return true;
              }
          }


          User

          這個(gè)就是我們要使用到的 User 對(duì)象,其中包含了 記住我,驗(yàn)證碼等登陸信息,因?yàn)?Spring Security 整合 Jwt 本質(zhì)上就是用自己自定義的登陸過濾器,去替換 Spring Security 原生的登陸過濾器,這樣的話,原生的記住我功能就會(huì)無法使用,所以我在 User 對(duì)象里添加了記住我的信息,用來自己實(shí)現(xiàn)這個(gè)功能。

          JWT 令牌認(rèn)證工具

          首先我們來新建一個(gè) TokenAuthenticationHelper 類,用來處理認(rèn)證過程中的驗(yàn)證和請(qǐng)求

          public class TokenAuthenticationHelper {
              /**
               * 未設(shè)置記住我時(shí) token 過期時(shí)間
               * */
              private static final long EXPIRATION_TIME= 7200000;
          
              /**
               * 記住我時(shí) cookie token 過期時(shí)間
               * */
              private static final int COOKIE_EXPIRATION_TIME= 1296000;
          
              private static final String SECRET_KEY= "ThisIsASpringSecurityDemo";
              public static final String COOKIE_TOKEN= "COOKIE-TOKEN";
              public static final String XSRF= "XSRF-TOKEN";
          
              /**
               * 設(shè)置登陸成功后令牌返回
               * */
              public static void addAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
                  // 獲取用戶登陸角色
                  Collection<? extends GrantedAuthority> authorities=authResult.getAuthorities();
                  // 遍歷用戶角色
                  StringBuffer stringBuffer= new StringBuffer();
                  authorities.forEach(authority -> {
                      stringBuffer.append(authority.getAuthority()).append(",");
                  });
                  long expirationTime=EXPIRATION_TIME;
                  int cookExpirationTime= -1;
                  // 處理登陸附加信息
                  LoginDetails loginDetails=(LoginDetails) authResult.getDetails();
                  if (loginDetails.getRememberMe() != null && loginDetails.getRememberMe()) {
                      expirationTime=COOKIE_EXPIRATION_TIME * 1000;
                      cookExpirationTime=COOKIE_EXPIRATION_TIME;
                  }
          
                  String jwt=Jwts.builder()
                          // Subject 設(shè)置用戶名
                          .setSubject(authResult.getName())
                          // 設(shè)置用戶權(quán)限
                          .claim("authorities", stringBuffer)
                          // 過期時(shí)間
                          .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
                          // 簽名算法
                          .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                          .compact();
                  Cookie cookie= new Cookie(COOKIE_TOKEN, jwt);
                  cookie.setHttpOnly(true);
                  cookie.setPath("/");
                  cookie.setMaxAge(cookExpirationTime);
                  response.addCookie(cookie);
          
                  // 向前端寫入數(shù)據(jù)
                  LoginResultDetails loginResultDetails= new LoginResultDetails();
                  ResultDetails resultDetails= new ResultDetails();
                  resultDetails.setStatus(HttpStatus.OK.value());
                  resultDetails.setMessage("登陸成功!");
                  resultDetails.setSuccess(true);
                  resultDetails.setTimestamp(LocalDateTime.now());
                  User user= new User();
                  user.setUsername(authResult.getName());
                  user.setPower(stringBuffer.toString());
                  user.setExpirationTime(System.currentTimeMillis() + expirationTime);
          
                  loginResultDetails.setResultDetails(resultDetails);
                  loginResultDetails.setUser(user);
                  loginResultDetails.setStatus(200);
                  response.setContentType("application/json; charset=UTF-8");
                  PrintWriter out=response.getWriter();
                  out.write(new ObjectMapper().writeValueAsString(loginResultDetails));
                  out.flush();
                  out.close();
              }
          
              /**
               * 對(duì)請(qǐng)求的驗(yàn)證
               * */
              public static Authentication getAuthentication(HttpServletRequest request) {
          
                  Cookie cookie=WebUtils.getCookie(request, COOKIE_TOKEN);
                  String token=cookie != null ? cookie.getValue() : null;
          
                  if (token != null) {
                      Claims claims=Jwts.parser()
                              .setSigningKey(SECRET_KEY)
                              .parseClaimsJws(token)
                              .getBody();
          
                      // 獲取用戶權(quán)限
                      Collection<? extends GrantedAuthority> authorities=                    Arrays.stream(claims.get("authorities").toString().split(","))
                                      .map(SimpleGrantedAuthority::new)
                                      .collect(Collectors.toList());
          
                      String userName=claims.getSubject();
                      if (userName != null) {
                          UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(userName, null, authorities);
                          usernamePasswordAuthenticationToken.setDetails(claims);
                          return usernamePasswordAuthenticationToken;
                      }
                      return null;
                  }
                  return null;
              }
          }

          TokenAuthenticationHelper

          1. addAuthentication 方法負(fù)責(zé)返回登陸成功的信息,使用 HTTP Only 的 Cookie 可以有效防止 XSS 攻擊。
          2. 登陸成功后返回用戶的權(quán)限,用戶名,登陸過期時(shí)間,可以有效的幫助前端構(gòu)建合適的用戶界面。
          3. getAuthentication 方法負(fù)責(zé)對(duì)用戶的其它請(qǐng)求進(jìn)行驗(yàn)證,如果用戶的 JWT 解析正確,則向 Spring Security 返回 usernamePasswordAuthenticationToken 用戶名密碼驗(yàn)證令牌,告訴 Spring Security 用戶所擁有的權(quán)限,并放到當(dāng)前的 Context 中,然后執(zhí)行過濾鏈?zhǔn)拐?qǐng)求繼續(xù)執(zhí)行下去。

          至此,我們的基本登陸與驗(yàn)證所需要的方法就寫完了

          ps:其中的 LoginResultDetails 類和 ResultDetails 請(qǐng)看項(xiàng)目源碼,篇幅所限,此處不在贅述。

          JWT 過濾器配置

          眾所周知,Spring Security 是借助一系列的 Servlet Filter 來來實(shí)現(xiàn)提供各種安全功能的,所以我們要使用 JWT 就需要自己實(shí)現(xiàn)兩個(gè)和 JWT 有關(guān)的過濾器

          1. 一個(gè)是用戶登錄的過濾器,在用戶的登錄的過濾器中校驗(yàn)用戶是否登錄成功,如果登錄成功,則生成一個(gè) token 返回給客戶端,登錄失敗則給前端一個(gè)登錄失敗的提示。
          2. 第二個(gè)過濾器則是當(dāng)其他請(qǐng)求發(fā)送來,校驗(yàn) token 的過濾器,如果校驗(yàn)成功,就讓請(qǐng)求繼續(xù)執(zhí)行。

          這兩個(gè)過濾器,我們分別來看,先看第一個(gè):

          在項(xiàng)目下新建一個(gè)包,名為 filter, 在 filter 下新建一個(gè)類名為 JwtLoginFilter, 并使其繼承 AbstractAuthenticationProcessingFilter 類,這個(gè)類是一個(gè)基于瀏覽器的基于 HTTP 的身份驗(yàn)證請(qǐng)求的抽象處理器。

          public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
              private final VerifyCodeService verifyCodeService;
          
              private final LoginCountService loginCountService;
          
              /**
               * @param defaultFilterProcessesUrl 配置要過濾的地址,即登陸地址
               * @param authenticationManager 認(rèn)證管理器,校驗(yàn)身份時(shí)會(huì)用到
               * @param loginCountService */
              public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager,
                                    VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
                  super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
                  this.loginCountService=loginCountService;
                  // 為 AbstractAuthenticationProcessingFilter 中的屬性賦值
                  setAuthenticationManager(authenticationManager);
                  this.verifyCodeService=verifyCodeService;
              }
          
          
          
              /**
               * 提取用戶賬號(hào)密碼進(jìn)行驗(yàn)證
               * */
              @Override
              public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
                  // 判斷是否要拋出 登陸請(qǐng)求過快的異常
                  loginCountService.judgeLoginCount(httpServletRequest);
                  // 獲取 User 對(duì)象
                  // readValue 第一個(gè)參數(shù) 輸入流,第二個(gè)參數(shù) 要轉(zhuǎn)換的對(duì)象
                  User user= new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
                  // 驗(yàn)證碼驗(yàn)證
                  verifyCodeService.verify(httpServletRequest.getSession().getId(), user.getVerifyCode());
                  // 對(duì) html 標(biāo)簽進(jìn)行轉(zhuǎn)義,防止 XSS 攻擊
                  String username=user.getUsername();
                  username=HtmlUtils.htmlEscape(username);
                  UsernamePasswordAuthenticationToken token= new UsernamePasswordAuthenticationToken(
                          username,
                          user.getPassword(),
                          user.getAuthorities()
                  );
                  // 添加驗(yàn)證的附加信息
                  // 包括驗(yàn)證碼信息和是否記住我
                  token.setDetails(new LoginDetails(user.getRememberMe(), user.getVerifyCode()));
                  // 進(jìn)行登陸驗(yàn)證
                  return getAuthenticationManager().authenticate(token);
              }
          
              /**
               * 登陸成功回調(diào)
               * */
              @Override
              protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
                  loginCountService.cleanLoginCount(request);
                  // 登陸成功
                  TokenAuthenticationHelper.addAuthentication(request, response ,authResult);
              }
          
              /**
               * 登陸失敗回調(diào)
               * */
              @Override
              protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
                  // 錯(cuò)誤請(qǐng)求次數(shù)加 1
                  loginCountService.addLoginCount(request, 1);
                  // 向前端寫入數(shù)據(jù)
                  ErrorDetails errorDetails= new ErrorDetails();
                  errorDetails.setStatus(HttpStatus.UNAUTHORIZED.value());
                  errorDetails.setMessage("登陸失敗!");
                  errorDetails.setError(failed.getLocalizedMessage());
                  errorDetails.setTimestamp(LocalDateTime.now());
                  errorDetails.setPath(request.getServletPath());
                  response.setContentType("application/json; charset=UTF-8");
                  PrintWriter out=response.getWriter();
                  out.write(new ObjectMapper().writeValueAsString(errorDetails));
                  out.flush();
                  out.close();
              }
          }

          JwtLoginFilter

          這個(gè)類主要有以下幾個(gè)作用

          1. 自定義 JwtLoginFilter 繼承自 AbstractAuthenticationProcessingFilter,并實(shí)現(xiàn)其中的三個(gè)默認(rèn)方法,其中的 defaultFilterProcessesUrl 變量就是我們需要設(shè)置的登陸路徑
          2. attemptAuthentication 方法中,我們從登錄參數(shù)中提取出用戶名密碼,然后調(diào)用 AuthenticationManager.authenticate() 方法去進(jìn)行自動(dòng)校驗(yàn)。
          3. 第二步如果校驗(yàn)成功,就會(huì)來到 successfulAuthentication 回調(diào)中,在 successfulAuthentication 方法中,使用之前已經(jīng)寫好的 addAuthentication 來生成 token,并使用 Http Only 的 cookie 寫出到客戶端。
          4. 第二步如果校驗(yàn)失敗就會(huì)來到 unsuccessfulAuthentication 方法中,在這個(gè)方法中返回一個(gè)錯(cuò)誤提示給客戶端即可。

          ps:其中的 verifyCodeService 與 loginCountService 方法與本文關(guān)系不大,其中的代碼實(shí)現(xiàn)請(qǐng)看源碼

          唯一需要注意的就是

          驗(yàn)證碼異常需要繼承 AuthenticationException 異常,

          可以看到這是一個(gè) Spring Security 各種異常的父類,寫一個(gè)驗(yàn)證碼異常類繼承 AuthenticationException,然后直接將驗(yàn)證碼異常拋出就好。

          以下完整代碼位于 com.bugaugaoshu.security.service.impl.DigitsVerifyCodeServiceImpl 類下

          @Override
          public void verify(String key, String code) {
                  String lastVerifyCodeWithTimestamp=verifyCodeRepository.find(key);
                  // 如果沒有驗(yàn)證碼,則隨機(jī)生成一個(gè)
                  if (lastVerifyCodeWithTimestamp== null) {
                      lastVerifyCodeWithTimestamp=appendTimestamp(randomDigitString(verifyCodeUtil.getLen()));
                  }
                  String[] lastVerifyCodeAndTimestamp=lastVerifyCodeWithTimestamp.split("#");
                  String lastVerifyCode=lastVerifyCodeAndTimestamp[0];
                  long timestamp=Long.parseLong(lastVerifyCodeAndTimestamp[1]);
                  if (timestamp + VERIFY_CODE_EXPIRE_TIMEOUT < System.currentTimeMillis()) {
                      throw new VerifyFailedException("驗(yàn)證碼已過期!");
                  } else if (!Objects.equals(code, lastVerifyCode)) {
                      throw new VerifyFailedException("驗(yàn)證碼錯(cuò)誤!");
                  }
              }

          DigitsVerifyCodeServiceImpl

          異常代碼在  com.bugaugaoshu.security.exception.VerifyFailedException 類下

          第二個(gè)用戶過濾器

          public class JwtAuthenticationFilter extends OncePerRequestFilter {
          
              @Override
              protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
                  try {
                      Authentication authentication=TokenAuthenticationHelper.getAuthentication(httpServletRequest);
          
                      // 對(duì)用 token 獲取到的用戶進(jìn)行校驗(yàn)
                      SecurityContextHolder.getContext().setAuthentication(authentication);
                      filterChain.doFilter(httpServletRequest, httpServletResponse);
                  } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException |
                          SignatureException | IllegalArgumentException e) {
                      httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired,登陸已過期");
                  }
              }
          }

          這個(gè)就很簡單了,將拿到的用戶 Token 進(jìn)行解析,如果正確,就將當(dāng)前用戶加入到 SecurityContext 的上下文中,授予用戶權(quán)限,否則返回 Token 過期的異常

          Spring Security 配置

          接下來我們來配置 Spring Security, 代碼如下

          @Configuration
          @EnableWebSecurity
          public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
              public static String ADMIN= "ROLE_ADMIN";
          
              public static String USER= "ROLE_USER";
          
              private final VerifyCodeService verifyCodeService;
          
              private final LoginCountService loginCountService;
          
              /**
               * 開放訪問的請(qǐng)求
               */
              private final static String[] PERMIT_ALL_MAPPING={
                      "/api/hello",
                      "/api/login",
                      "/api/home",
                      "/api/verifyImage",
                      "/api/image/verify",
                      "/images/**"
              };
          
              public WebSecurityConfig(VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
                  this.verifyCodeService=verifyCodeService;
                  this.loginCountService=loginCountService;
              }
          
              @Bean
              public PasswordEncoder passwordEncoder() {
                  return new BCryptPasswordEncoder();
              }
          
              /**
               * 跨域配置
               */
              @Bean
              public CorsConfigurationSource corsConfigurationSource() {
                  // 允許跨域訪問的 URL
                  List<String> allowedOriginsUrl= new ArrayList<>();
                  allowedOriginsUrl.add("http://localhost:8080");
                  allowedOriginsUrl.add("http://127.0.0.1:8080");
                  CorsConfiguration config= new CorsConfiguration();
                  config.setAllowCredentials(true);
                  // 設(shè)置允許跨域訪問的 URL
                  config.setAllowedOrigins(allowedOriginsUrl);
                  config.addAllowedHeader("*");
                  config.addAllowedMethod("*");
                  UrlBasedCorsConfigurationSource source= new UrlBasedCorsConfigurationSource();
                  source.registerCorsConfiguration("/**", config);
                  return source;
              }
          
              @Override
              protected void configure(HttpSecurity http) throws Exception {
                  http.authorizeRequests()
                          .antMatchers(PERMIT_ALL_MAPPING)
                          .permitAll()
                          .antMatchers("/api/user/**", "/api/data", "/api/logout")
                          // USER 和 ADMIN 都可以訪問
                          .hasAnyAuthority(USER, ADMIN)
                          .antMatchers("/api/admin/**")
                          // 只有 ADMIN 才可以訪問
                          .hasAnyAuthority(ADMIN)
                          .anyRequest()
                          .authenticated()
                          .and()
                          // 添加過濾器鏈,前一個(gè)參數(shù)過濾器, 后一個(gè)參數(shù)過濾器添加的地方
                          // 登陸過濾器
                          .addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager(), verifyCodeService, loginCountService), UsernamePasswordAuthenticationFilter.class)
                          // 請(qǐng)求過濾器
                          .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                          // 開啟跨域
                          .cors()
                          .and()
                          // 開啟 csrf
                          .csrf()
                          // .disable();
                          .ignoringAntMatchers(PERMIT_ALL_MAPPING)
                          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
              }
          
              @Override
              public void configure(WebSecurity web) throws Exception {
                  super.configure(web);
              }
          
              @Override
              protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                  // 在內(nèi)存中寫入用戶數(shù)據(jù)
                  auth.
                          authenticationProvider(daoAuthenticationProvider());
                          //.inMemoryAuthentication();
          // .withUser("user")
          // .password(passwordEncoder().encode("123456"))
          // .authorities("ROLE_USER")
          // .and()
          // .withUser("admin")
          // .password(passwordEncoder().encode("123456"))
          // .authorities("ROLE_ADMIN")
          // .and()
          // .withUser("block")
          // .password(passwordEncoder().encode("123456"))
          // .authorities("ROLE_USER")
          // .accountLocked(true);
              }
          
              @Bean
              public DaoAuthenticationProvider daoAuthenticationProvider() {
          
                  DaoAuthenticationProvider provider= new DaoAuthenticationProvider();
                  provider.setHideUserNotFoundExceptions(false);
                  provider.setPasswordEncoder(passwordEncoder());
                  provider.setUserDetailsService(new CustomUserDetailsService());
                  return provider;
              }

          以上代碼的注釋很詳細(xì),我就不多說了,重點(diǎn)說一下兩個(gè)地方一個(gè)是 csrf 的問題,另一個(gè)就是 inMemoryAuthentication 在內(nèi)存中寫入用戶的部分。

          首先說 csrf 的問題:我看了看網(wǎng)上有很多 Spring Security 的教程,都會(huì)將 .csrf()設(shè)置為 .disable() , 這種設(shè)置雖然方便,但是不夠安全,忽略了使用安全框架的初衷所以為了安全起見,我還是開啟了這個(gè)功能,順便學(xué)習(xí)一下如何使用 XSRF-TOKEN

          因?yàn)檫@個(gè)項(xiàng)目是一個(gè) Demo, 不涉及數(shù)據(jù)庫部分,所以我選擇了在內(nèi)存中直接寫入用戶,網(wǎng)上的向內(nèi)存中寫入用戶如上代碼注釋部分,這樣寫雖然簡單,但是有一些問題,在打個(gè)斷點(diǎn)我們就能知道種方式調(diào)用的是 Spring Security 的是 ProviderManager 這個(gè)方法,這種方法不方便我們拋出入用戶名不存在或者其異常,它都會(huì)拋出 Bad Credentials 異常,不會(huì)提示其它錯(cuò)誤, 如下圖所示。

          Spring Security 為了安全考慮,會(huì)把所有的登陸異常全部歸結(jié)為 Bad Credentials 異常,所以為了能拋出像用戶名不存在的這種異常,如果采用 Spring Security 默認(rèn)的登陸方式的話, 可以采用像 GitHub 項(xiàng)目 Vhr 里的這種處理方式,但是因?yàn)檫@個(gè)項(xiàng)目使用 Jwt 替換掉了默認(rèn)的登陸方式,想要實(shí)現(xiàn)詳細(xì)的異常信息拋出就比較復(fù)雜了,我找了好久也沒找到比較簡單且合適的方法。如果你有好的方法,歡迎分享。

          最后我的解決方案是使用 Spring Security 的 DaoAuthenticationProvider 這個(gè)類來成為認(rèn)證提供者,這個(gè)類實(shí)現(xiàn)了 AbstractUserDetailsAuthenticationProvider 這一個(gè)抽象的用戶詳細(xì)信息身份驗(yàn)證功能,查看注釋我們可以知道 AbstractUserDetailsAuthenticationProvider 提供了 A base AuthenticationProvider that allows subclasses to override and work with UserDetails objects. The class is designed to respond to UsernamePasswordAuthenticationToken authentication requests.(允許子類重寫和使用 UserDetails 對(duì)象的基本身份驗(yàn)證提供程序。該類旨在響應(yīng) UsernamePasswordAuthenticationToken 身份驗(yàn)證請(qǐng)求。)

          通過配置自定義的用戶查詢實(shí)現(xiàn)類,我們可以直接在 CustomUserDetailsService 里拋出沒有發(fā)現(xiàn)用戶名的異常,然后再設(shè)置 hideUserNotFoundExceptions 為 false 這樣就可以區(qū)別是密碼錯(cuò)誤,還是用戶名不存在的錯(cuò)誤了,

          但是這種方式還是有一個(gè)問題,不能拋出像賬戶被鎖定這種異常,理論上這種功能可以繼承 AbstractUserDetailsAuthenticationProvider 這個(gè)抽象類然后自己重寫的登陸方法來實(shí)現(xiàn),我看了看好像比較復(fù)雜,一個(gè) Demo 沒必要,我就放棄了。

          另外據(jù)說安全信息暴露的越少越好,所以暫時(shí)就先這樣吧。(算是給自己找個(gè)理由)

          用戶查找服務(wù)

          public class CustomUserDetailsService implements UserDetailsService {
              private List<UserDetails> userList= new ArrayList<>();
          
              public CustomUserDetailsService() {
                  PasswordEncoder passwordEncoder= new BCryptPasswordEncoder();
                  UserDetails user=User.withUsername("user").password(passwordEncoder.encode("123456")).authorities(WebSecurityConfig.USER).build();
                  UserDetails admin=User.withUsername("admin").password(passwordEncoder.encode("123456")).authorities(WebSecurityConfig.ADMIN).build();
                  userList.add(user);
                  userList.add(admin);
              }
          
              @Override
              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                  for (UserDetails userDetails : userList) {
                      if (userDetails.getUsername().equals(username)) {
                          // 此處我嘗試過直接返回 user
                          // 但是這樣的話,只有后臺(tái)服務(wù)啟動(dòng)后第一次登陸會(huì)有效
                          // 推出后第二次登陸會(huì)出現(xiàn) Empty encoded password 的錯(cuò)誤,導(dǎo)致無法登陸
                          // 這樣寫就不會(huì)出現(xiàn)這種問題了
                          // 因?yàn)樵诘谝淮悟?yàn)證后,用戶的密碼會(huì)被清除,導(dǎo)致第二次登陸系統(tǒng)拿到的是空密碼
                          // 所以需要new一個(gè)對(duì)象或?qū)⒃瓕?duì)象復(fù)制一份
                          // 這個(gè)解決方案來自 https://stackoverflow.com/questions/43007763/spring-security-encoded-password-gives-me-bad-credentials/43046195#43046195
                          return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
                      }
                  }
                  throw new UsernameNotFoundException("用戶名不存在,請(qǐng)檢查用戶名或注冊(cè)!");
              }
          }

          這部分就比較簡單了,唯一的注意點(diǎn)我在注釋中已經(jīng)寫的很清楚了,當(dāng)然你要是使用連接數(shù)據(jù)庫的話,這個(gè)問題就不存在了。

          UserDetailsService 這個(gè)接口就是 Spring Security 為其它的數(shù)據(jù)訪問策略做支持的。

          至此,一個(gè)基本的 Spring Security + JWT 登陸的后端就完成了,你可以寫幾個(gè) controller 然后用 postman 測試功能了。

          其它部分的代碼因?yàn)楸容^簡單,你可以參照源碼自行實(shí)現(xiàn)你需要的功能。

          前端搭建

          創(chuàng)建 Vue 項(xiàng)目的方式網(wǎng)上有很多,此處也不再贅述,我只說一點(diǎn),過去 Vue 項(xiàng)目創(chuàng)建完成后,在項(xiàng)目目錄下會(huì)生成一個(gè) config 文件夾,用來存放 vue 的配置,但現(xiàn)在默認(rèn)創(chuàng)建的項(xiàng)目是不會(huì)生成這個(gè)文件夾的,需要你手動(dòng)在項(xiàng)目根目錄下創(chuàng)建 vue.config.js 作為配置文件。

          此處請(qǐng)參考:Vue CLI 官方文檔,配置參考部分

          附:使用 Vue CIL 創(chuàng)建 Vue 項(xiàng)目

          依賴包

          前后端數(shù)據(jù)傳遞我使用了更為簡單的 fetch api, 當(dāng)然你也可以選擇兼容性更加好的 axios

          Ui 為 ElementUI

          為了獲取 XSRF-TOKEN,還需要 VueCookies

          最后為了在項(xiàng)目的首頁展示介紹,我還引入了 mavonEditor,一個(gè)基于 vue 的 Markdown 插件

          引入以上包之后,你與要修改 src 目錄下的 main.js 文件如下。

          import Vue from 'vue'
          import App from './App.vue'
          import router from './router'
          import store from './store'
          import ElementUI from 'element-ui'
          import 'element-ui/lib/theme-chalk/index.css'
          import mavonEditor from 'mavon-editor';
          import 'mavon-editor/dist/css/index.css';
          import VueCookies from 'vue-cookies'
          import axios from 'axios'
          
          // 讓ajax攜帶cookie
          axios.defaults.withCredentials=true;
          // 注冊(cè) axios 為全局變量
          Vue.prototype.$axios=axios
          // 使用 vue cookie
          Vue.use(VueCookies)
          Vue.config.productionTip= false
          // 使用 ElementUI 組件
          Vue.use(ElementUI)
          // markdown 解析編輯工具
          Vue.use(mavonEditor)
          // 后臺(tái)服務(wù)地址
          Vue.prototype.SERVER_API_URL= "http://127.0.0.1:8088/api";
          
          
          new Vue({
              router,
              store,
              render: h => h(App)
          }).$mount('#app')

          前端跨域配置

          在創(chuàng)建 vue.config.js 完成后,你需要在里面輸入以下內(nèi)容,用來完成 Vue 的跨域配置

          module.exports ={
              // options...
              devServer: {
                proxy: {
                    '/api': {
                        target: 'http://127.0.0.1:8088',
                        changeOrigin: true,
                        ws: true,
                        pathRewrite:{
                          '^/api':'' 
                       }
                    }
                }
            }
          }

          一些注意事項(xiàng)

          頁面設(shè)計(jì)這些沒有什么可寫的了,需要注意的一點(diǎn)就是在對(duì)后端服務(wù)器進(jìn)行 POST,DELETE,PUT 等操作時(shí),請(qǐng)?jiān)谡?qǐng)求頭中帶上 "X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN'), 如果不帶,那么哪怕你登陸了,后臺(tái)也會(huì)返回 403 異常的。

          credentials: "include" 這句也不能少,這是攜帶 Cookie 所必須的語句。如果不加這一句,等于沒有攜帶 Cookie,也就等于沒有登陸了。

          舉個(gè)例子:

          deleteItem(data) {
              fetch(this.SERVER_API_URL + "/admin/data/" + data.id, {
                  headers: {
                      "Content-Type": "application/json; charset=UTF-8",
                      "X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN')
                  },
                  method: "DELETE",
                  credentials: "include"
              }).then(response => response.json())
                  .then(json => {
                      if (json.status=== 200) {
                          this.systemDataList.splice(data.id, 1);
                          this.$message({
                              message: '刪除成功',
                              type: 'success'
                          });
                      } else {
                          window.console.log(json);
                          this.$message.error(json.message);
                      }
                  });
          },

          結(jié)束

          作者:陜西顏值扛把子 來源:知乎 github完整代碼可私信獲取!

          tml網(wǎng)頁源碼加密

          html網(wǎng)頁源碼能加密嗎?能加密到何種程度?

          某些時(shí)候,我們可能需要對(duì)html網(wǎng)頁源碼加密,使網(wǎng)頁源碼不那么容易被他人獲得。出于這個(gè)目標(biāo),本文測試一種html加密方式。

          提前透露:結(jié)論超出預(yù)期,似乎還實(shí)現(xiàn)了反爬蟲。

          首先來到網(wǎng)址:http://fairysoftware.com/html_jia_mi.html

          由頁面介紹可知,這是一種使用js和escape結(jié)合實(shí)現(xiàn)的html加密。

          直接使用頁面提供的例程,加密這一段html代碼:

          得到加密的html代碼,如下圖:

          然后將加密代碼粘貼到一個(gè)html文件中測試,如下圖:

          頁面可以正常打開。查看網(wǎng)頁源碼,果然源碼是加密的,如下圖:

          特別的驚喜之處是:

          如上圖所示,鏈接果然消失了。

          即使用開發(fā)者工具查看,也無法得到鏈接地址,而原始未加密前的html代碼中是有鏈接的,如下圖:

          那么消失了的鏈接,還能正常點(diǎn)擊嗎?

          點(diǎn)擊,鏈接可以正常打開:

          雖然href鏈接隱藏了,但還能正常打開頁面,功能完全正常。

          測試結(jié)果既驚喜又意外,這樣的html網(wǎng)頁加密,效果還真是不錯(cuò),值得一用。


          主站蜘蛛池模板: 亚洲a∨无码一区二区| 日本精品一区二区三区在线观看| 波霸影院一区二区| 亚洲一区二区三区在线视频| 亚洲一区二区三区高清| 精品久久久久久无码中文字幕一区 | 精品国产一区二区三区免费| 日韩精品无码一区二区三区不卡| 精品一区二区在线观看| 亚洲综合av一区二区三区| 国产日韩精品一区二区在线观看| 久久国产高清一区二区三区| 亚洲AV无码一区东京热久久| 97se色综合一区二区二区| 无码喷水一区二区浪潮AV| 精品乱码一区二区三区在线| 色久综合网精品一区二区| 无码人妻AⅤ一区二区三区水密桃 无码欧精品亚洲日韩一区夜夜嗨 无码毛片一区二区三区中文字幕 无码毛片一区二区三区视频免费播放 | 国产精品区一区二区三| 日韩精品人妻一区二区三区四区| 久久国产高清一区二区三区| 中文字幕无线码一区2020青青| 久久精品国产一区| 国产在线精品观看一区| 精品国产一区二区三区麻豆| 91麻豆精品国产自产在线观看一区| 无码精品黑人一区二区三区| 在线精品日韩一区二区三区| 色噜噜狠狠一区二区三区| 亚洲无人区一区二区三区| 国产精品亚洲综合一区在线观看 | 日本一区精品久久久久影院| 女同一区二区在线观看| 精品国产一区二区三区无码| 精品人妻码一区二区三区| 少妇精品久久久一区二区三区| 日韩免费观看一区| 亚洲第一区视频在线观看| 亚洲AV无码一区东京热久久 | 无码人妻啪啪一区二区| 日韩精品一区二区三区老鸭窝|