詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
原來一直使用shiro做安全框架,配置起來相當(dāng)方便,正好有機(jī)會(huì)接觸下SpringSecurity,學(xué)習(xí)下這個(gè)。順道結(jié)合下jwt,把安全信息管理的問題扔給客戶端,
準(zhǔn)備
首先用的是SpringBoot,省去寫各種xml的時(shí)間。然后把依賴加入一下
<!--安全--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
application.yml加上一點(diǎn)配置信息,后面會(huì)用
jwt: secret: secret expiration: 7200000 token: Authorization
可能用到代碼,目錄結(jié)構(gòu)放出來一下

配置
SecurityConfig配置
首先是配置SecurityConfig,代碼如下
@Configuration
@EnableWebSecurity// 這個(gè)注解必須加,開啟Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保證post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtUserDetailsService jwtUserDetailsService;
@Autowired
JwtAuthorizationTokenFilter authenticationTokenFilter;
//先來這里認(rèn)證一下
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoderBean());
}
//攔截在這配
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/haha").permitAll()
.antMatchers("/sysUser/test").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
.anyRequest().authenticated() // 剩下所有的驗(yàn)證都需要驗(yàn)證
.and()
.csrf().disable() // 禁用 Spring Security 自帶的跨域處理
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 定制我們自己的 session 策略:調(diào)整為讓 Spring Security 不創(chuàng)建和使用 session
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
ok,下面娓娓道來。首先我們這個(gè)配置類繼承了WebSecurityConfigurerAdapter,這里面有三個(gè)重要的方法需要我們重寫一下:
configure(HttpSecurity http):這個(gè)方法是我們配置攔截的地方,exceptionHandling().authenticationEntryPoint(),這里面主要配置如果沒有憑證,可以進(jìn)行一些操作,這個(gè)后面會(huì)看jwtAuthenticationEntryPoint這個(gè)里面的代碼。進(jìn)行下一項(xiàng)配置,為了區(qū)分必須加入.and()。authorizeRequests()這個(gè)后邊配置那些路徑有需要什么權(quán)限,比如我配置的那幾個(gè)url都是permitAll(),及不需要權(quán)限就可以訪問。值得一提的是antMatchers(HttpMethod.OPTIONS, "/**"),是為了方便后面寫前后端分離的時(shí)候前端過來的第一次驗(yàn)證請(qǐng)求,這樣做,會(huì)減少這種請(qǐng)求的時(shí)間和資源使用。csrf().disable()是為了防止csdf攻擊的,至于什么是csdf攻擊,請(qǐng)自行百度。
另起一行,以示尊重。sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);因?yàn)槲覀円褂胘wt托管安全信息,所以把Session禁止掉??聪耂essionCreationPolicy枚舉的幾個(gè)參數(shù):
public enum SessionCreationPolicy {
ALWAYS,//總是會(huì)新建一個(gè)Session。
NEVER,//不會(huì)新建HttpSession,但是如果有Session存在,就會(huì)使用它。
IF_REQUIRED,//如果有要求的話,會(huì)新建一個(gè)Session。
STATELESS;//這個(gè)是我們用的,不會(huì)新建,也不會(huì)使用一個(gè)HttpSession。
private SessionCreationPolicy() {
}
}
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);這行代碼主要是用于JWT驗(yàn)證,后面再說。
configure(WebSecurity web):這個(gè)方法我代碼中沒有用,這個(gè)方法主要用于訪問一些靜態(tài)的東西控制。其中ignoring()方法可以讓訪問跳過filter驗(yàn)證。configureGlobal(AuthenticationManagerBuilder auth):這個(gè)方法是主要進(jìn)行驗(yàn)證的地方,其中jwtUserDetailsService代碼待會(huì)會(huì)看,passwordEncoder(passwordEncoderBean())是密碼的一種加密方式。
還有兩個(gè)注解:@EnableWebSecurity,這個(gè)注解必須加,開啟Security。
@EnableGlobalMethodSecurity(prePostEnabled = true),保證post之前的注解可以使用
以上,我們可以確定了哪些路徑訪問不需要任何權(quán)限了,至于哪些路徑需要什么權(quán)限接著往下看。
SecurityUserDetails
Security 中也有類似于shiro中主體的概念,就是在內(nèi)存中存了一個(gè)東西,方便程序判斷當(dāng)前請(qǐng)求的用戶有什么權(quán)限,需要實(shí)現(xiàn)UserDetails這個(gè)接口,所以我寫了這個(gè)類,并且繼承了我自己的類SysUser。
public enum SessionCreationPolicy {
ALWAYS,//總是會(huì)新建一個(gè)Session。
NEVER,//不會(huì)新建HttpSession,但是如果有Session存在,就會(huì)使用它。
IF_REQUIRED,//如果有要求的話,會(huì)新建一個(gè)Session。
STATELESS;//這個(gè)是我們用的,不會(huì)新建,也不會(huì)使用一個(gè)HttpSession。
private SessionCreationPolicy() {
}
}
authorities就是我們的權(quán)限,構(gòu)造方法中我手動(dòng)把密碼set進(jìn)去了,這不合適,包括權(quán)限我也是手動(dòng)傳進(jìn)去的。這些東西都應(yīng)該從數(shù)據(jù)庫搜出來,我現(xiàn)在只是體驗(yàn)一把Security,角色權(quán)限那一套都沒寫,所以說明一下就好了,這個(gè)構(gòu)造方法就是傳進(jìn)來一個(gè)標(biāo)志(我這里用的是username,或者應(yīng)該用userId什么的都可以),然后給你一個(gè)完整的主體信息,供其他地方使用。ok,next。
JwtUserDetailsService
SecurityConfig配置里面不是有個(gè)方法是做真正的認(rèn)證嘛,或者說從數(shù)據(jù)庫拿信息,具體那認(rèn)證信息的方法就是在這個(gè)方法里面。
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String user) throws UsernameNotFoundException {
System.out.println("JwtUserDetailsService:" + user);
List<GrantedAuthority> authorityList = new ArrayList<>();
authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
return new SecurityUserDetails(user,authorityList);
}
}
繼承了Security提供的UserDetailsService接口,實(shí)現(xiàn)loadUserByUsername這個(gè)方法,我們這里手動(dòng)模擬從數(shù)據(jù)庫搜出來一個(gè)叫USER的權(quán)限,通過剛才的構(gòu)造方法,模擬生成當(dāng)前user的信息,供后面jwt Filter一大堆驗(yàn)證。至于為什么USER權(quán)限要加上“ROLE_”前綴,待會(huì)會(huì)說。
ok,現(xiàn)在我們知道了怎么配置各種url是否需要權(quán)限才能訪問,也知道了哪里可以拿到我們的主體信息,那么繼續(xù)。
JwtAuthorizationTokenFilter
千呼萬喚始出來,JWT終于可以上場了。至于怎么生成這個(gè)token憑證,待會(huì)會(huì)說,現(xiàn)在假設(shè)前端已經(jīng)拿到了token憑證,要訪問某個(gè)接口了,看看怎么進(jìn)行jwt業(yè)務(wù)的攔截吧。
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private final String tokenHeader;
public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService") UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil, @Value("${jwt.token}") String tokenHeader) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (ExpiredJwtException e) {
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
提前說一下,關(guān)于@Value注解參數(shù)開頭寫了。
doFilterInternal() 這個(gè)方法就是這個(gè)過濾器的精髓了。首先從header中獲取憑證authToken,從中挖掘出來我們的username,然后看看上下文中是否有我們以這個(gè)username為標(biāo)識(shí)的主體。沒有,ok,去new一個(gè)(如果對(duì)象也可以new就好了。。。)。然后就是驗(yàn)證這個(gè)authToken 是否在有效期呢啊,驗(yàn)證token是否對(duì)啊等等吧。其實(shí)我們剛剛把我們SecurityUserDetails這個(gè)對(duì)象叫做主體,到這里我才發(fā)現(xiàn)有點(diǎn)自做多情了,因?yàn)樯蒘ecurity承認(rèn)的主體是通過UsernamePasswordAuthenticationToken類似與這種類去實(shí)現(xiàn)的,之前之所以叫SecurityUserDetails為主體,只是它存了一些關(guān)鍵信息。然后將主體信息————authentication,存入上下文環(huán)境,供后面使用。
我的很多工具類代碼都放到了jwtTokenUtil,下面貼一下代碼:
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.token}")
private String tokenHeader;
private Clock clock = DefaultClock.INSTANCE;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration);
}
public Boolean validateToken(String token, UserDetails userDetails) {
SecurityUserDetails user = (SecurityUserDetails) userDetails;
final String username = getUsernameFromToken(token);
return (username.equals(user.getUsername())
&& !isTokenExpired(token)
);
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
}
根據(jù)注釋你能猜個(gè)大概吧,就不再說了,有些東西是jwt方面的東西,今天就不再多說了。
JwtAuthenticationEntryPoint
前面還說了一個(gè)發(fā)現(xiàn)沒有憑證走一個(gè)方法,代碼也貼一下。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
System.out.println("JwtAuthenticationEntryPoint:"+authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"沒有憑證");
}
}
實(shí)現(xiàn)AuthenticationEntryPoint這個(gè)接口,發(fā)現(xiàn)沒有憑證,往response中放些東西。
run code
下面跑一下幾個(gè)接口,看看具體是怎么具體訪問某個(gè)方法的吧,還有前面一點(diǎn)懸念一并解決。
登錄
先登錄一下,看看怎么生成token扔給前端的吧。
@RestController
public class LoginController {
@Autowired
@Qualifier("jwtUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
@PostMapping("haha")
public String haha(){
UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return "haha:"+userDetails.getUsername()+","+userDetails.getPassword();
}
}
我們前面配置中已經(jīng)把login設(shè)置為隨便訪問了,這邊通過jwt生成一個(gè)token串,具體方法請(qǐng)看jwtTokenUtil.generateToken,已經(jīng)寫了。只要知道這里面存了username、加密規(guī)則、過期時(shí)間就好了。
然后跑下haha接口,發(fā)現(xiàn)沒問題,正常打印,說明主體也在上下文中了。
需要權(quán)限
然后我們?cè)L問一個(gè)需要權(quán)限的接口吧。
@RestController
@RequestMapping("/sysUser")
public class SysUserController {
@GetMapping(value = "/test")
public String test() {
return "Hello Spring Security";
}
@PreAuthorize("hasAnyRole('USER')")
@PostMapping(value = "/testNeed")
public String testNeed() {
return "testNeed";
}
}
訪問testNeed接口,看到?jīng)],@PreAuthorize("hasAnyRole('USER')")這個(gè)說明需要USER權(quán)限!我們?cè)趧倓偵蒘ecurityUserDetails這個(gè)的時(shí)候已經(jīng)模擬加入了USER權(quán)限了,所以可以訪問。現(xiàn)在說說為什么加權(quán)限的時(shí)候需要加入前綴“ROLE_”.看hasAnyRole源碼:
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
}
if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(defaultRolePrefix)) {
return role;
}
return defaultRolePrefix + role;
}
關(guān)鍵是 defaultRolePrefix 看這個(gè)類最上面
private String defaultRolePrefix = "ROLE_";
人家源碼這么干的,咱們就這么寫唄,咱也不敢問。其實(shí)也有不需要前綴的方式,去看看SecurityExpressionRoot這個(gè)類吧,用的方法不一樣,也就是@PreAuthorize里面有另外一個(gè)參數(shù)。
一個(gè)重要的問題
先說結(jié)論:Security上下文環(huán)境(里面有主體)生命周期只限于一次請(qǐng)求。
我做了一個(gè)測試:
把SecurityConfig里面configure(HttpSecurity http)這個(gè)方法里面
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
這行代碼注釋掉,不走那個(gè)jwt filter。就是不每次都添加上下上下文環(huán)境。
然后loginController改成
@RestController
public class LoginController {
@Autowired
@Qualifier("jwtUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
//添加 start
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
//添加 end
return token;
}
@PostMapping("haha")
public String haha(){
UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return "haha:"+userDetails.getUsername()+","+userDetails.getPassword();
}
}
然后登陸,然后訪問/haha,崩了,發(fā)現(xiàn)userDetails里面沒數(shù)據(jù)。說明這會(huì)上下文環(huán)境中我們主體不存在。
為什么會(huì)這樣呢?
SecurityContextPersistenceFilter 一次請(qǐng)求,filter鏈結(jié)束之后 會(huì)清除掉Context里面的東西。所說以,主體數(shù)據(jù)生命周期是一次請(qǐng)求。
源碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...假裝有一堆代碼...
try {
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
}
關(guān)鍵就是finally里面 SecurityContextHolder.clearContext(); 這句話。這才體現(xiàn)了那句,把維護(hù)信息的事扔給了客戶端,你不請(qǐng)求,我也不知道你有啥。
體驗(yàn)小結(jié)
配置起來感覺還可以吧,使用jwt方式,生成token.由于上下文環(huán)境的生命周期是一次請(qǐng)求,所以在不請(qǐng)求的情況下,服務(wù)端不清楚用戶有那些權(quán)限,真正實(shí)現(xiàn)了客戶端維護(hù)安全信息,所以項(xiàng)目中也沒有登出接口,因?yàn)闆]必要。即使前端退出了,你有token,依然可以通過postman請(qǐng)求接口(token沒有過期)。不同于shiro可以把信息維護(hù)在服務(wù)端,要是登出,clear主體信息,訪問接口就需要在登錄。不過Security這樣也有好處,可以實(shí)現(xiàn)單點(diǎn)登陸了,也方便做分布式。(只要你不同子系統(tǒng)中驗(yàn)證那一套邏輯相同,或者在分布式的情況下有單獨(dú)的驗(yàn)證系統(tǒng))。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- Springboot WebFlux集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的示例
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringBoot Security+JWT簡單搭建的實(shí)現(xiàn)示例
相關(guān)文章
SpringCloud?Feign請(qǐng)求頭刪除修改的操作代碼
這篇文章主要介紹了SpringCloud?Feign請(qǐng)求頭刪除修改,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
不規(guī)范使用ThreadLocal導(dǎo)致bug分析解決
這篇文章主要為大家介紹了不規(guī)范使用ThreadLocal導(dǎo)致bug分析解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
基于Java Socket實(shí)現(xiàn)一個(gè)簡易在線聊天功能(一)
這篇文章主要給大家介紹基于Java Socket實(shí)現(xiàn)一個(gè)簡易在線聊天功能(一),分為客戶端和服務(wù)端兩段代碼,非常具有參考價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-05-05
SpringBoot整合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信功能
在當(dāng)今互聯(lián)網(wǎng)時(shí)代,實(shí)時(shí)通信已經(jīng)成為了許多應(yīng)用程序的基本需求,而WebSocket作為一種全雙工通信協(xié)議,為開發(fā)者提供了一種簡單、高效的實(shí)時(shí)通信解決方案,本文將介紹如何使用SpringBoot框架來實(shí)現(xiàn)WebSocket的集成,快速搭建實(shí)時(shí)通信功能,感興趣的朋友可以參考下2023-11-11
SpringBoot項(xiàng)目設(shè)置斷點(diǎn)debug調(diào)試無效忽略web.xml問題的解決
這篇文章主要介紹了SpringBoot項(xiàng)目設(shè)置斷點(diǎn)debug調(diào)試無效忽略web.xml問題的解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
多線程計(jì)數(shù),怎么保持計(jì)數(shù)準(zhǔn)確的方法
這篇文章主要介紹了多線程計(jì)數(shù)的方法,有需要的朋友可以參考一下2014-01-01
springboot實(shí)現(xiàn)跨域的五種方式總結(jié)
在Spring Boot中實(shí)現(xiàn)跨域,可以采用全局跨域和局部跨域兩種方式,下面這篇文章主要給大家介紹了關(guān)于springboot實(shí)現(xiàn)跨域的五種方式,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法
本篇文章主要介紹了詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04

