最近在項目中遇到了這樣一個問題:先後端分離,前端用Vue來作,全部的數據請求都使用vue-resource,沒有使用表單,所以數據交互都是使用JSON,後臺使用Spring Boot,權限驗證使用了Spring Security,由於以前用Spring Security都是處理頁面的,此次單純處理Ajax請求,所以記錄下遇到的一些問題。這裏的解決方案不只適用於Ajax請求,也能夠解決移動端請求驗證。css
首先咱們須要建立一個Spring Boot工程,建立時須要引入Web、Spring Security、MySQL和MyBatis(數據庫框架其實隨意,我這裏使用MyBatis),建立好以後,依賴文件以下:前端
<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>
注意最後一個commons-codec依賴是我手動加入進來的,這是一個Apache的開源項目,能夠用來生成MD5消息摘要,我在後文中將對密碼進行簡單的處理。vue
爲了簡化邏輯,我這裏建立了三個表,分別是用戶表、角色表、用戶角色關聯表,以下: java
接下來咱們須要在application.properties中對本身的數據庫進行簡單的配置,這裏各位小夥伴視本身的具體狀況而定。mysql
spring.datasource.url=jdbc:mysql:///vueblog spring.datasource.username=root spring.datasource.password=123
這裏主要是指構造用戶類,這裏的用戶類比較特殊,必須實現UserDetails接口,以下:web
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省略... }
實現了UserDetails接口以後,該接口中有幾個方法須要咱們實現,四個返回Boolean的方法都是見名知意,enabled表示檔期帳戶是否啓用,這個我數據庫中確實有該字段,所以根據查詢結果返回,其餘的爲了簡單期間都直接返回true,getAuthorities方法返回當前用戶的角色信息,用戶的角色其實就是roles中的數據,將roles中的數據轉換爲List<GrantedAuthority>以後返回便可,這裏有一個要注意的地方,因爲我在數據庫中存儲的角色名都是諸如‘超級管理員’、‘普通用戶’之類的,並非以ROLE_
這樣的字符開始的,所以須要在這裏手動加上ROLE_
,切記。 spring
另外還有一個Role實體類,比較簡單,按照數據庫的字段建立便可,這裏再也不贅述。sql
這裏的UserService也比較特殊,須要實現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,這裏返回一個不含有任何值的User對象,在後期的密碼比對過程當中同樣會驗證失敗 return new User(); } //查詢用戶的角色信息,並返回存入user中 List<Role> roles = rolesMapper.getRolesByUid(user.getId()); user.setRoles(roles); return user; } }
實現了UserDetailsService接口以後,咱們須要實現該接口中的loadUserByUsername方法,即根據用戶名查詢用戶。這裏注入了兩個MyBatis中的Mapper,UserMapper用來查詢用戶,RolesMapper用來查詢角色。在loadUserByUsername方法中,首先根據傳入的參數(參數就是用戶登陸時輸入的用戶名)去查詢用戶,若是查到的用戶爲null,能夠直接拋一個UsernameNotFoundException異常,可是我爲了處理方便,返回了一個沒有任何值的User對象,這樣在後面的密碼比對過程當中同樣會發現登陸失敗的(這裏你們根據本身的業務需求調整便可),若是查到的用戶不爲null,此時咱們根據查到的用戶id再去查詢該用戶的角色,並將查詢結果放入到user對象中,這個查詢結果將在user對象的getAuthorities方法中用上。json
咱們先來看一下個人Security配置,而後我再來一一解釋:
@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("超級管理員") .anyRequest().authenticated()//其餘的路徑都是登陸後便可訪問 .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"); } }
這是咱們配置的核心,小夥伴們聽我一一道來:
1.首先這是一個配置類,所以記得加上@Configuration註解,又由於這是Spring Security的配置,所以記得繼承WebSecurityConfigurerAdapter。
2.將剛剛建立好的UserService注入進來,一會咱們要用。
3.configure(AuthenticationManagerBuilder auth)方法中用來配置咱們的認證方式,在auth.userDetailsService()方法中傳入userService,這樣userService中的loadUserByUsername方法在用戶登陸時將會被自動調用。後面的passwordEncoder是可選項,可寫可不寫,由於我是將用戶的明文密碼生成了MD5消息摘要後存入數據庫的,所以在登陸時也須要對明文密碼進行處理,因此就加上了passwordEncoder,加上passwordEncoder後,直接new一個PasswordEncoder匿名內部類便可,這裏有兩個方法要實現,看名字就知道方法的含義,第一個方法encode顯然是對明文進行加密,這裏我使用了MD5消息摘要,具體的實現方法是由commons-codec依賴提供的;第二個方法matches是密碼的比對,兩個參數,第一個參數是明文密碼,第二個是密文,這裏只須要對明文加密後和密文比較便可(小夥伴若是對此感興趣能夠繼續考慮密碼加鹽)。
4.configure(HttpSecurity http)用來配置咱們的認證規則等,authorizeRequests方法表示開啓了認證規則配置,antMatchers("/admin/**").hasRole("超級管理員")表示/admin/**
的路徑須要有‘超級管理員’角色的用戶才能訪問,我在網上看到小夥伴對hasRole方法中要不要加ROLE_
前綴有疑問,這裏是不要加的,若是用hasAuthority方法才須要加。anyRequest().authenticated()表示其餘全部路徑都是須要認證/登陸後才能訪問。接下來咱們配置了登陸頁面爲login_page,登陸處理路徑爲/login,登陸用戶名爲username,密碼爲password,並配置了這些路徑均可以直接訪問,註銷登錄也能夠直接訪問,最後關閉csrf。在successHandler中,使用response返回登陸成功的json便可,切記不可使用defaultSuccessUrl,defaultSuccessUrl是隻登陸成功後重定向的頁面,使用failureHandler也是因爲相同的緣由。
5.configure(WebSecurity web)方法中我配置了一些過濾規則,不贅述。
6.另外,對於靜態文件,如/images/**
、/css/**
、/js/**
這些路徑,這裏默認都是不攔截的。
最後來看看咱們的Controller,以下:
@RestController public class LoginRegController { /** * 若是自動跳轉到這個頁面,說明用戶未登陸,返回相應的提示便可 * <p> * 若是要支持表單登陸,能夠在這個方法中判斷請求的類型,進而決定返回JSON仍是HTML頁面 * * @return */ @RequestMapping("/login_page") public RespBean loginPage() { return new RespBean("error", "還沒有登陸,請登陸!"); } }
這個Controller總體來講仍是比較簡單的,RespBean一個響應bean,返回一段簡單的json,不贅述,這裏須要小夥伴注意的是login_page
,咱們配置的登陸頁面是一個login_page
,但實際上login_page
並非一個頁面,而是返回一段JSON,這是由於當我未登陸就去訪問其餘頁面時Spring Security會自動跳轉到到login_page
頁面,可是在Ajax請求中,不須要這種跳轉,我要的只是是否登陸的提示,因此這裏返回json便可。
最後小夥伴可使用POSTMAN或者RESTClient等工具來測試登陸和權限問題,我就不演示了。
Ok,通過上文的介紹,想必小夥伴們對Spring Boot+Spring Security處理Ajax登陸請求已經有所瞭解了,好了,本文就說到這裏,有問題歡迎留言討論。
更多資料請關注公衆號: