您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)SpringBoot+SpringSecurity處理Ajax登錄請(qǐng)求的示例分析的內(nèi)容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。
最近在項(xiàng)目中遇到了這樣一個(gè)問(wèn)題:前后端分離,前端用Vue來(lái)做,所有的數(shù)據(jù)請(qǐng)求都使用vue-resource,沒(méi)有使用表單,因此數(shù)據(jù)交互都是使用JSON,后臺(tái)使用Spring Boot,權(quán)限驗(yàn)證使用了Spring Security,因?yàn)橹坝肧pring Security都是處理頁(yè)面的,這次單純處理Ajax請(qǐng)求,因此記錄下遇到的一些問(wèn)題。這里的解決方案不僅適用于Ajax請(qǐng)求,也可以解決移動(dòng)端請(qǐng)求驗(yàn)證。
創(chuàng)建工程
首先我們需要?jiǎng)?chuàng)建一個(gè)Spring Boot工程,創(chuàng)建時(shí)需要引入Web、Spring Security、MySQL和MyBatis(數(shù)據(jù)庫(kù)框架其實(shí)隨意,我這里使用MyBatis),創(chuàng)建好之后,依賴(lài)文件如下:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency>
注意最后一個(gè) commons-codec 依賴(lài)是我手動(dòng)加入進(jìn)來(lái)的,這是一個(gè)Apache的開(kāi)源項(xiàng)目,可以用來(lái)生成MD5消息摘要,我在后文中將對(duì)密碼進(jìn)行簡(jiǎn)單的處理。
創(chuàng)建數(shù)據(jù)庫(kù)并配置
為了簡(jiǎn)化邏輯,我這里創(chuàng)建了三個(gè)表,分別是用戶(hù)表、角色表、用戶(hù)角色關(guān)聯(lián)表,如下:
接下來(lái)我們需要在application.properties中對(duì)自己的數(shù)據(jù)庫(kù)進(jìn)行簡(jiǎn)單的配置,這里各位小伙伴視自己的具體情況而定。
spring.datasource.url=jdbc:mysql:///vueblog spring.datasource.username=root spring.datasource.password=123
構(gòu)造實(shí)體類(lèi)
這里主要是指構(gòu)造用戶(hù)類(lèi),這里的用戶(hù)類(lèi)比較特殊,必須實(shí)現(xiàn)UserDetails接口,如下:
public class User implements UserDetails { private Long id; private String username; private String password; private String nickname; private boolean enabled; private List<Role> roles; @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } @Override public List<GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); } return authorities; } //getter/setter省略... }
實(shí)現(xiàn)了UserDetails接口之后,該接口中有幾個(gè)方法需要我們實(shí)現(xiàn),四個(gè)返回Boolean的方法都是見(jiàn)名知意,enabled表示檔期賬戶(hù)是否啟用,這個(gè)我數(shù)據(jù)庫(kù)中確實(shí)有該字段,因此根據(jù)查詢(xún)結(jié)果返回,其他的為了簡(jiǎn)單期間都直接返回true,getAuthorities方法返回當(dāng)前用戶(hù)的角色信息,用戶(hù)的角色其實(shí)就是roles中的數(shù)據(jù),將roles中的數(shù)據(jù)轉(zhuǎn)換為L(zhǎng)ist<GrantedAuthority>之后返回即可, 這里有一個(gè)要注意的地方,由于我在數(shù)據(jù)庫(kù)中存儲(chǔ)的角色名都是諸如‘超級(jí)管理員'、‘普通用戶(hù)'之類(lèi)的,并不是以 ROLE_
這樣的字符開(kāi)始的,因此需要在這里手動(dòng)加上 ROLE_
,切記 。
另外還有一個(gè)Role實(shí)體類(lèi),比較簡(jiǎn)單,按照數(shù)據(jù)庫(kù)的字段創(chuàng)建即可,這里不再贅述。
創(chuàng)建UserService
這里的UserService也比較特殊,需要實(shí)現(xiàn)UserDetailsService接口,如下:
@Service public class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Autowired RolesMapper rolesMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(s); if (user == null) { //避免返回null,這里返回一個(gè)不含有任何值的User對(duì)象,在后期的密碼比對(duì)過(guò)程中一樣會(huì)驗(yàn)證失敗 return new User(); } //查詢(xún)用戶(hù)的角色信息,并返回存入user中 List<Role> roles = rolesMapper.getRolesByUid(user.getId()); user.setRoles(roles); return user; } }
實(shí)現(xiàn)了UserDetailsService接口之后,我們需要實(shí)現(xiàn)該接口中的loadUserByUsername方法,即根據(jù)用戶(hù)名查詢(xún)用戶(hù)。這里注入了兩個(gè)MyBatis中的Mapper,UserMapper用來(lái)查詢(xún)用戶(hù),RolesMapper用來(lái)查詢(xún)角色。在loadUserByUsername方法中,首先根據(jù)傳入的參數(shù)(參數(shù)就是用戶(hù)登錄時(shí)輸入的用戶(hù)名)去查詢(xún)用戶(hù),如果查到的用戶(hù)為null,可以直接拋一個(gè)UsernameNotFoundException異常,但是我為了處理方便,返回了一個(gè)沒(méi)有任何值的User對(duì)象,這樣在后面的密碼比對(duì)過(guò)程中一樣會(huì)發(fā)現(xiàn)登錄失敗的(這里大家根據(jù)自己的業(yè)務(wù)需求調(diào)整即可),如果查到的用戶(hù)不為null,此時(shí)我們根據(jù)查到的用戶(hù)id再去查詢(xún)?cè)撚脩?hù)的角色,并將查詢(xún)結(jié)果放入到user對(duì)象中,這個(gè)查詢(xún)結(jié)果將在user對(duì)象的getAuthorities方法中用上。
Security配置
我們先來(lái)看一下我的Security配置,然后我再來(lái)一一解釋?zhuān)?/p>
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } /** * @param charSequence 明文 * @param s 密文 * @return */ @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes())); } }); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("超級(jí)管理員") .anyRequest().authenticated()//其他的路徑都是登錄后即可訪問(wèn) .and().formLogin().loginPage("/login_page").successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{\"status\":\"ok\",\"msg\":\"登錄成功\"}"); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"登錄失敗\"}"); out.flush(); out.close(); } }).loginProcessingUrl("/login") .usernameParameter("username").passwordParameter("password").permitAll() .and().logout().permitAll().and().csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/reg"); } }
這是我們配置的核心,小伙伴們聽(tīng)我一一道來(lái):
1.首先這是一個(gè)配置類(lèi),因此記得加上@Configuration注解,又因?yàn)檫@是Spring Security的配置,因此記得繼承WebSecurityConfigurerAdapter。
2.將剛剛創(chuàng)建好的UserService注入進(jìn)來(lái),一會(huì)我們要用。
3.configure(AuthenticationManagerBuilder auth)方法中用來(lái)配置我們的認(rèn)證方式,在auth.userDetailsService()方法中傳入userService,這樣userService中的loadUserByUsername方法在用戶(hù)登錄時(shí)將會(huì)被自動(dòng)調(diào)用。后面的passwordEncoder是可選項(xiàng),可寫(xiě)可不寫(xiě),因?yàn)槲沂菍⒂脩?hù)的明文密碼生成了MD5消息摘要后存入數(shù)據(jù)庫(kù)的,因此在登錄時(shí)也需要對(duì)明文密碼進(jìn)行處理,所以就加上了passwordEncoder,加上passwordEncoder后,直接new一個(gè)PasswordEncoder匿名內(nèi)部類(lèi)即可,這里有兩個(gè)方法要實(shí)現(xiàn),看名字就知道方法的含義,第一個(gè)方法encode顯然是對(duì)明文進(jìn)行加密,這里我使用了MD5消息摘要,具體的實(shí)現(xiàn)方法是由commons-codec依賴(lài)提供的;第二個(gè)方法matches是密碼的比對(duì),兩個(gè)參數(shù),第一個(gè)參數(shù)是明文密碼,第二個(gè)是密文,這里只需要對(duì)明文加密后和密文比較即可(小伙伴如果對(duì)此感興趣可以繼續(xù)考慮密碼加鹽)。
4.configure(HttpSecurity http)用來(lái)配置我們的認(rèn)證規(guī)則等,authorizeRequests方法表示開(kāi)啟了認(rèn)證規(guī)則配置,antMatchers("/admin/**").hasRole("超級(jí)管理員")表示 /admin/**
的路徑需要有‘超級(jí)管理員'角色的用戶(hù)才能訪問(wèn),我在網(wǎng)上看到小伙伴對(duì)hasRole方法中要不要加 ROLE_
前綴有疑問(wèn),這里是不要加的,如果用hasAuthority方法才需要加。anyRequest().authenticated()表示其他所有路徑都是需要認(rèn)證/登錄后才能訪問(wèn)。接下來(lái)我們配置了登錄頁(yè)面為login_page,登錄處理路徑為/login,登錄用戶(hù)名為username,密碼為password,并配置了這些路徑都可以直接訪問(wèn),注銷(xiāo)登陸也可以直接訪問(wèn),最后關(guān)閉csrf。在successHandler中,使用response返回登錄成功的json即可,切記不可以使用defaultSuccessUrl,defaultSuccessUrl是只登錄成功后重定向的頁(yè)面,使用failureHandler也是由于相同的原因。
5.configure(WebSecurity web)方法中我配置了一些過(guò)濾規(guī)則,不贅述。
6.另外,對(duì)于靜態(tài)文件,如 /images/**
、 /css/**
、 /js/**
這些路徑,這里默認(rèn)都是不攔截的。
Controller
最后來(lái)看看我們的Controller,如下:
@RestController public class LoginRegController { /** * 如果自動(dòng)跳轉(zhuǎn)到這個(gè)頁(yè)面,說(shuō)明用戶(hù)未登錄,返回相應(yīng)的提示即可 * <p> * 如果要支持表單登錄,可以在這個(gè)方法中判斷請(qǐng)求的類(lèi)型,進(jìn)而決定返回JSON還是HTML頁(yè)面 * * @return */ @RequestMapping("/login_page") public RespBean loginPage() { return new RespBean("error", "尚未登錄,請(qǐng)登錄!"); } }
這個(gè)Controller整體來(lái)說(shuō)還是比較簡(jiǎn)單的,RespBean一個(gè)響應(yīng)bean,返回一段簡(jiǎn)單的json,不贅述,這里需要小伙伴注意的是 login_page
,我們配置的登錄頁(yè)面是一個(gè) login_page
,但實(shí)際上 login_page
并不是一個(gè)頁(yè)面,而是返回一段JSON,這是因?yàn)楫?dāng)我未登錄就去訪問(wèn)其他頁(yè)面時(shí)Spring Security會(huì)自動(dòng)跳轉(zhuǎn)到到 login_page
頁(yè)面,但是在Ajax請(qǐng)求中,不需要這種跳轉(zhuǎn),我要的只是是否登錄的提示,所以這里返回json即可。
測(cè)試
最后小伙伴可以使用POSTMAN或者RESTClient等工具來(lái)測(cè)試登錄和權(quán)限問(wèn)題,我就不演示了。
感謝各位的閱讀!關(guān)于“SpringBoot+SpringSecurity處理Ajax登錄請(qǐng)求的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。