Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
行展示
主要展示 Spring Security 與 JWT 結(jié)合使用構(gòu)建后端 API 接口。
主要功能包括登陸(如何在 Spring Security 中添加驗(yàn)證碼登陸),查找,創(chuàng)建,刪除并對(duì)用戶權(quán)限進(jìn)行區(qū)分等等。
ps:由于只是 Demo,所以沒(méi)有調(diào)用數(shù)據(jù)庫(kù),以上所說(shuō)增刪改查均在 HashMap 中完成。
展示如何使用 Vue 構(gòu)建前端后與后端的配合,包括跨域的設(shè)置,前端登陸攔截
并實(shí)現(xiàn) POST,GET,DELETE 請(qǐng)求。包括如何在 Vue 中使用后端的 XSRF-TOKEN 防范 CSRF 攻擊
實(shí)現(xiàn)細(xì)節(jié)
創(chuàng)建 Spring boot 項(xiàng)目,添加 JJWT 和 Spring Security 的項(xiàng)目依賴,這個(gè)非常簡(jiǎn)單,有很多的教程都有塊內(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è)簡(jiǎn)單的 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ì)上就是用自己自定義的登陸過(guò)濾器,去替換 Spring Security 原生的登陸過(guò)濾器,這樣的話,原生的記住我功能就會(huì)無(wú)法使用,所以我在 User 對(duì)象里添加了記住我的信息,用來(lái)自己實(shí)現(xiàn)這個(gè)功能。
首先我們來(lái)新建一個(gè) TokenAuthenticationHelper 類,用來(lái)處理認(rèn)證過(guò)程中的驗(yàn)證和請(qǐng)求
public class TokenAuthenticationHelper {
/**
* 未設(shè)置記住我時(shí) token 過(guò)期時(shí)間
* */
private static final long EXPIRATION_TIME = 7200000;
/**
* 記住我時(shí) cookie token 過(guò)期時(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)
// 過(guò)期時(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
至此,我們的基本登陸與驗(yàn)證所需要的方法就寫完了
ps:其中的 LoginResultDetails 類和 ResultDetails 請(qǐng)看項(xiàng)目源碼,篇幅所限,此處不在贅述。
眾所周知,Spring Security 是借助一系列的 Servlet Filter 來(lái)來(lái)實(shí)現(xiàn)提供各種安全功能的,所以我們要使用 JWT 就需要自己實(shí)現(xiàn)兩個(gè)和 JWT 有關(guān)的過(guò)濾器
這兩個(gè)過(guò)濾器,我們分別來(lái)看,先看第一個(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 配置要過(guò)濾的地址,即登陸地址
* @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)求過(guò)快的異常
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è)作用
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);
// 如果沒(méi)有驗(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)證碼已過(guò)期!");
} else if (!Objects.equals(code, lastVerifyCode)) {
throw new VerifyFailedException("驗(yàn)證碼錯(cuò)誤!");
}
}
DigitsVerifyCodeServiceImpl
異常代碼在 com.bugaugaoshu.security.exception.VerifyFailedException 類下
第二個(gè)用戶過(guò)濾器
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,登陸已過(guò)期");
}
}
}
這個(gè)就很簡(jiǎn)單了,將拿到的用戶 Token 進(jìn)行解析,如果正確,就將當(dāng)前用戶加入到 SecurityContext 的上下文中,授予用戶權(quán)限,否則返回 Token 過(guò)期的異常
接下來(lái)我們來(lái)配置 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;
/**
* 開放訪問(wèn)的請(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() {
// 允許跨域訪問(wèn)的 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è)置允許跨域訪問(wèn)的 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 都可以訪問(wèn)
.hasAnyAuthority(USER, ADMIN)
.antMatchers("/api/admin/**")
// 只有 ADMIN 才可以訪問(wèn)
.hasAnyAuthority(ADMIN)
.anyRequest()
.authenticated()
.and()
// 添加過(guò)濾器鏈,前一個(gè)參數(shù)過(guò)濾器, 后一個(gè)參數(shù)過(guò)濾器添加的地方
// 登陸過(guò)濾器
.addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager(), verifyCodeService, loginCountService), UsernamePasswordAuthenticationFilter.class)
// 請(qǐng)求過(guò)濾器
.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ì),我就不多說(shuō)了,重點(diǎn)說(shuō)一下兩個(gè)地方一個(gè)是 csrf 的問(wèn)題,另一個(gè)就是 inMemoryAuthentication 在內(nèi)存中寫入用戶的部分。
首先說(shuō) csrf 的問(wèn)題:我看了看網(wǎng)上有很多 Spring Security 的教程,都會(huì)將 .csrf()設(shè)置為 .disable() , 這種設(shè)置雖然方便,但是不夠安全,忽略了使用安全框架的初衷所以為了安全起見,我還是開啟了這個(gè)功能,順便學(xué)習(xí)一下如何使用 XSRF-TOKEN
因?yàn)檫@個(gè)項(xiàng)目是一個(gè) Demo, 不涉及數(shù)據(jù)庫(kù)部分,所以我選擇了在內(nèi)存中直接寫入用戶,網(wǎng)上的向內(nèi)存中寫入用戶如上代碼注釋部分,這樣寫雖然簡(jiǎn)單,但是有一些問(wèn)題,在打個(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ù)雜了,我找了好久也沒(méi)找到比較簡(jiǎn)單且合適的方法。如果你有好的方法,歡迎分享。
最后我的解決方案是使用 Spring Security 的 DaoAuthenticationProvider 這個(gè)類來(lái)成為認(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)求。)
通過(guò)配置自定義的用戶查詢實(shí)現(xiàn)類,我們可以直接在 CustomUserDetailsService 里拋出沒(méi)有發(fā)現(xiàn)用戶名的異常,然后再設(shè)置 hideUserNotFoundExceptions 為 false 這樣就可以區(qū)別是密碼錯(cuò)誤,還是用戶名不存在的錯(cuò)誤了,
但是這種方式還是有一個(gè)問(wèn)題,不能拋出像賬戶被鎖定這種異常,理論上這種功能可以繼承 AbstractUserDetailsAuthenticationProvider 這個(gè)抽象類然后自己重寫的登陸方法來(lái)實(shí)現(xiàn),我看了看好像比較復(fù)雜,一個(gè) Demo 沒(méi)必要,我就放棄了。
另外據(jù)說(shuō)安全信息暴露的越少越好,所以暫時(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)) {
// 此處我嘗試過(guò)直接返回 user
// 但是這樣的話,只有后臺(tái)服務(wù)啟動(dòng)后第一次登陸會(huì)有效
// 推出后第二次登陸會(huì)出現(xiàn) Empty encoded password 的錯(cuò)誤,導(dǎo)致無(wú)法登陸
// 這樣寫就不會(huì)出現(xiàn)這種問(wèn)題了
// 因?yàn)樵诘谝淮悟?yàn)證后,用戶的密碼會(huì)被清除,導(dǎo)致第二次登陸系統(tǒng)拿到的是空密碼
// 所以需要new一個(gè)對(duì)象或?qū)⒃瓕?duì)象復(fù)制一份
// 這個(gè)解決方案來(lái)自 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è)!");
}
}
這部分就比較簡(jiǎn)單了,唯一的注意點(diǎn)我在注釋中已經(jīng)寫的很清楚了,當(dāng)然你要是使用連接數(shù)據(jù)庫(kù)的話,這個(gè)問(wèn)題就不存在了。
UserDetailsService 這個(gè)接口就是 Spring Security 為其它的數(shù)據(jù)訪問(wèn)策略做支持的。
至此,一個(gè)基本的 Spring Security + JWT 登陸的后端就完成了,你可以寫幾個(gè) controller 然后用 postman 測(cè)試功能了。
其它部分的代碼因?yàn)楸容^簡(jiǎn)單,你可以參照源碼自行實(shí)現(xiàn)你需要的功能。
創(chuàng)建 Vue 項(xiàng)目的方式網(wǎng)上有很多,此處也不再贅述,我只說(shuō)一點(diǎn),過(guò)去 Vue 項(xiàng)目創(chuàng)建完成后,在項(xiàng)目目錄下會(huì)生成一個(gè) config 文件夾,用來(lái)存放 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ù)傳遞我使用了更為簡(jiǎn)單的 fetch api, 當(dāng)然你也可以選擇兼容性更加好的 axios
Ui 為 ElementUI
為了獲取 XSRF-TOKEN,還需要 VueCookies
最后為了在項(xiàng)目的首頁(yè)展示介紹,我還引入了 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)容,用來(lái)完成 Vue 的跨域配置
module.exports = {
// options...
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8088',
changeOrigin: true,
ws: true,
pathRewrite:{
'^/api':''
}
}
}
}
}
頁(yè)面設(shè)計(jì)這些沒(méi)有什么可寫的了,需要注意的一點(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 所必須的語(yǔ)句。如果不加這一句,等于沒(méi)有攜帶 Cookie,也就等于沒(méi)有登陸了。
舉個(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);
}
});
},
作者:陜西顏值扛把子 來(lái)源:知乎 github完整代碼可私信獲取!
人對(duì)超過(guò)50000個(gè)github上的開源java項(xiàng)目做了統(tǒng)計(jì),統(tǒng)計(jì)出最常用的16個(gè)開源工具類及其方法。
大部分方法可以望文知意,請(qǐng)務(wù)必瀏覽一遍,知道有哪些好用的工具類,不必自己造輪子了。
絕對(duì)的好東西,直接收藏吧
下面是已經(jīng)按照使用次數(shù)排序的列表,
1. org.apache.commons.io.IOUtils
2. org.apache.commons.io.FileUtils
3. org.apache.commons.lang.StringUtils
4. org.apache.http.util.EntityUtils
5. org.apache.commons.lang3.StringUtils
6. org.apache.commons.io.FilenameUtils
7. org.springframework.util.StringUtils
8. org.apache.commons.lang.ArrayUtils
9. org.apache.commons.lang.StringEscapeUtils
10. org.apache.http.client.utils.URLEncodedUtils
11. org.apache.commons.codec.digest.DigestUtils
12. org.apache.commons.collections.CollectionUtils
13. org.apache.commons.lang3.ArrayUtils
14. org.apache.commons.beanutils.PropertyUtils
15. org.apache.commons.lang3.StringEscapeUtils
16. org.apache.commons.beanutils.BeanUtils
. org.apache.commons.io.IOUtils
closeQuietly:關(guān)閉一個(gè)IO流、socket、或者selector且不拋出異常,通常放在finally塊 toString:轉(zhuǎn)換IO流、 Uri、 byte[]為String copy:IO流數(shù)據(jù)復(fù)制,從輸入流寫到輸出流中,最大支持2GB toByteArray:從輸入流、URI獲取byte[] write:把字節(jié). 字符等寫入輸出流 toInputStream:把字符轉(zhuǎn)換為輸入流 readLines:從輸入流中讀取多行數(shù)據(jù),返回List<String> copyLarge:同copy,支持2GB以上數(shù)據(jù)的復(fù)制 lineIterator:從輸入流返回一個(gè)迭代器,根據(jù)參數(shù)要求讀取的數(shù)據(jù)量,全部讀取,如果數(shù)據(jù)不夠,則失敗
deleteDirectory:刪除文件夾 readFileToString:以字符形式讀取文件內(nèi)容 deleteQueitly:刪除文件或文件夾且不會(huì)拋出異常 copyFile:復(fù)制文件 writeStringToFile:把字符寫到目標(biāo)文件,如果文件不存在,則創(chuàng)建 forceMkdir:強(qiáng)制創(chuàng)建文件夾,如果該文件夾父級(jí)目錄不存在,則創(chuàng)建父級(jí) write:把字符寫到指定文件中 listFiles:列舉某個(gè)目錄下的文件(根據(jù)過(guò)濾器) copyDirectory:復(fù)制文件夾 forceDelete:強(qiáng)制刪除文件
isBlank:字符串是否為空 (trim后判斷) isEmpty:字符串是否為空 (不trim并判斷) equals:字符串是否相等 join:合并數(shù)組為單一字符串,可傳分隔符 split:分割字符串 EMPTY:返回空字符串 trimToNull:trim后為空字符串則轉(zhuǎn)換為null replace:替換字符串
toString:把Entity轉(zhuǎn)換為字符串 consume:確保Entity中的內(nèi)容全部被消費(fèi)。可以看到源碼里又一次消費(fèi)了Entity的內(nèi)容,假如用戶沒(méi)有消費(fèi),那調(diào)用Entity時(shí)候?qū)?huì)把它消費(fèi)掉 toByteArray:把Entity轉(zhuǎn)換為字節(jié)流 consumeQuietly:和consume一樣,但不拋異常 getContentCharset:獲取內(nèi)容的編碼
isBlank:字符串是否為空 (trim后判斷) isEmpty:字符串是否為空 (不trim并判斷) equals:字符串是否相等 join:合并數(shù)組為單一字符串,可傳分隔符 split:分割字符串 EMPTY:返回空字符串 replace:替換字符串 capitalize:首字符大寫
getExtension:返回文件后綴名 getBaseName:返回文件名,不包含后綴名 getName:返回文件全名 concat:按命令行風(fēng)格組合文件路徑(詳見方法注釋) removeExtension:刪除后綴名 normalize:使路徑正常化 wildcardMatch:匹配通配符 seperatorToUnix:路徑分隔符改成unix系統(tǒng)格式的,即/ getFullPath:獲取文件路徑,不包括文件名 isExtension:檢查文件后綴名是不是傳入?yún)?shù)(List<String>)中的一個(gè)
hasText:檢查字符串中是否包含文本 hasLength:檢測(cè)字符串是否長(zhǎng)度大于0 isEmpty:檢測(cè)字符串是否為空(若傳入為對(duì)象,則判斷對(duì)象是否為null) commaDelimitedStringToArray:逗號(hào)分隔的String轉(zhuǎn)換為數(shù)組 collectionToDelimitedString:把集合轉(zhuǎn)為CSV格式字符串 replace 替換字符串 7. delimitedListToStringArray:相當(dāng)于split uncapitalize:首字母小寫 collectionToDelimitedCommaString:把集合轉(zhuǎn)為CSV格式字符串 tokenizeToStringArray:和split基本一樣,但能自動(dòng)去掉空白的單詞
contains:是否包含某字符串 addAll:添加整個(gè)數(shù)組 clone:克隆一個(gè)數(shù)組 isEmpty:是否空數(shù)組 add:向數(shù)組添加元素 subarray:截取數(shù)組 indexOf:查找某個(gè)元素的下標(biāo) isEquals:比較數(shù)組是否相等 toObject:基礎(chǔ)類型數(shù)據(jù)數(shù)組轉(zhuǎn)換為對(duì)應(yīng)的Object數(shù)組
參考十五:org.apache.commons.lang3.StringEscapeUtils
format:格式化參數(shù),返回一個(gè)HTTP POST或者HTTP PUT可用application/x-www-form-urlencoded字符串 parse:把String或者URI等轉(zhuǎn)換為L(zhǎng)ist<NameValuePair>
md5Hex:MD5加密,返回32位字符串 sha1Hex:SHA-1加密 sha256Hex:SHA-256加密 sha512Hex:SHA-512加密 md5:MD5加密,返回16位字符串
isEmpty:是否為空 select:根據(jù)條件篩選集合元素 transform:根據(jù)指定方法處理集合元素,類似List的map() filter:過(guò)濾元素,雷瑟List的filter() find:基本和select一樣 collect:和transform 差不多一樣,但是返回新數(shù)組 forAllDo:調(diào)用每個(gè)元素的指定方法 isEqualCollection:判斷兩個(gè)集合是否一致
contains:是否包含某個(gè)字符串 addAll:添加整個(gè)數(shù)組 clone:克隆一個(gè)數(shù)組 isEmpty:是否空數(shù)組 add:向數(shù)組添加元素 subarray:截取數(shù)組 indexOf:查找某個(gè)元素的下標(biāo) isEquals:比較數(shù)組是否相等 toObject:基礎(chǔ)類型數(shù)據(jù)數(shù)組轉(zhuǎn)換為對(duì)應(yīng)的Object數(shù)組
getProperty:獲取對(duì)象屬性值 setProperty:設(shè)置對(duì)象屬性值 getPropertyDiscriptor:獲取屬性描述器 isReadable:檢查屬性是否可訪問(wèn) copyProperties:復(fù)制屬性值,從一個(gè)對(duì)象到另一個(gè)對(duì)象 getPropertyDiscriptors:獲取所有屬性描述器 isWriteable:檢查屬性是否可寫 getPropertyType:獲取對(duì)象屬性類型
unescapeHtml4:轉(zhuǎn)義html escapeHtml4:反轉(zhuǎn)義html escapeXml:轉(zhuǎn)義xml unescapeXml:反轉(zhuǎn)義xml escapeJava:轉(zhuǎn)義unicode編碼 escapeEcmaScript:轉(zhuǎn)義EcmaScript字符 unescapeJava:反轉(zhuǎn)義unicode編碼 escapeJson:轉(zhuǎn)義json字符 escapeXml10:轉(zhuǎn)義Xml10
這個(gè)現(xiàn)在已經(jīng)廢棄了,建議使用commons-text包里面的方法。
copyPeoperties:復(fù)制屬性值,從一個(gè)對(duì)象到另一個(gè)對(duì)象 getProperty:獲取對(duì)象屬性值 setProperty:設(shè)置對(duì)象屬性值 populate:根據(jù)Map給屬性復(fù)制 copyPeoperty:復(fù)制單個(gè)值,從一個(gè)對(duì)象到另一個(gè)對(duì)象 cloneBean:克隆bean實(shí)例
感謝閱讀,如果這篇文章幫助了您,歡迎 點(diǎn)贊 ,收藏,關(guān)注,轉(zhuǎn)發(fā) 喲。您的幫助是我們前行的動(dòng)力,我們會(huì)提供更多有價(jià)值的內(nèi)容給大家... 謝謝!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。