若是不是先後端分離項目,使用SpringSecurity作登陸功能會很省心,只要簡單的幾項配置,即可以輕鬆完成登陸成功失敗的處理,當訪問須要認證的頁面時,能夠自動重定向到登陸頁面。可是先後端分離的項目就不同了,不能直接由後臺處理,而是要向前端返回相應的json提示。前端
在本例的介紹中,主要解決了如下幾個問題:java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
package com.hanstrovsky.config; ... /** * @author Hanstrovsky */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // security默認不支持註解的方式的權限控制,加上這個註解開啓 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final MyUserDetailsService myUserDetailsService; private final MyPasswordEncoder myPasswordEncoder; public WebSecurityConfig(MyUserDetailsService myUserDetailsService, MyPasswordEncoder myPasswordEncoder) { this.myUserDetailsService = myUserDetailsService; this.myPasswordEncoder = myPasswordEncoder; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 定義加密解密方式 auth.userDetailsService(myUserDetailsService).passwordEncoder(myPasswordEncoder); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .httpBasic() // 訪問須要認證的url,進行json提示 .and().exceptionHandling() .authenticationEntryPoint((req, resp, e) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "未登陸或登陸超時!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); }) .and() .authorizeRequests() .anyRequest().authenticated()// 必須認證以後才能訪問 .and() .formLogin()// 表單登陸 .permitAll() // 和表單登陸相關的接口通通都直接經過 .and() .logout().deleteCookies("JSESSIONID")// 註銷登陸,刪除cookie // 自定義註銷成功,返回json .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "註銷成功!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }) .and() // session 超時返回json提示 .sessionManagement() .maximumSessions(5).maxSessionsPreventsLogin(true)// 同一用戶最大同時在線數量5個,超出後阻止登陸 // session 超時返回json提示 .expiredSessionStrategy(new SessionInformationExpiredStrategy() { @Override public void onExpiredSessionDetected( SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException { HttpServletResponse resp = sessionInformationExpiredEvent.getResponse(); // 返回提示 resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "登陸超時!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }); //用重寫的Filter替換掉原有的UsernamePasswordAuthenticationFilter http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);} //註冊自定義的UsernamePasswordAuthenticationFilter,使用json格式數據登陸 @Bean CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); // 自定義登陸成功或失敗 返回json提示 filter.setAuthenticationSuccessHandler((req, resp, authentication) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "登陸成功!"); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); }); filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); String errorMessage = "登陸失敗"; FrontResult frontResult = FrontResult.init(FrontResult.FAILED, errorMessage); out.write(new ObjectMapper().writeValueAsString(frontResult)); out.flush(); out.close(); } }); filter.setFilterProcessesUrl("/user/login"); //重用WebSecurityConfigurerAdapter配置的AuthenticationManager,否則要本身組裝AuthenticationManager filter.setAuthenticationManager(authenticationManagerBean()); return filter; } }
security默認提供了Basic和表單兩種登陸方式,不支持Json格式的數據,須要對處理登陸的過濾器進行修改。這裏,咱們重寫了UsernamePasswordAuthenticationFilter的attemptAuthentication方法。web
package com.hanstrovsky.filter; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.Map; /** * 自定義過濾器,重寫 attemptAuthentication方法,實現使用json格式的數據進行登陸 * * @author Hanstrovsky */ @Slf4j public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { ObjectMapper mapper = new ObjectMapper(); UsernamePasswordAuthenticationToken authRequest = null; try (InputStream is = request.getInputStream()) { Map<String, String> authenticationBean = mapper.readValue(is, Map.class); String username = authenticationBean.get("username"); String password = authenticationBean.get("password"); authRequest = new UsernamePasswordAuthenticationToken( username, password); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken( "", ""); } setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } else { // 保留原來的方法 return super.attemptAuthentication(request, response); } } }
這個接口是用來提供用戶名和密碼的,能夠經過查詢數據庫獲取用戶。本例直接在代碼中寫死。spring
package com.hanstrovsky.service; import com.hanstrovsky.entity.MyUserDetails; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Repository; /** * @author Hanstrovsky */ @Repository public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { // 能夠在此處自定義從數據庫查詢用戶 MyUserDetails myUserDetail = new MyUserDetails(); myUserDetail.setUsername(username); myUserDetail.setPassword("123456"); return myUserDetail; } }
自定義密碼的加密方式。數據庫
package com.hanstrovsky.util; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * 自定義的密碼加密方法,實現了PasswordEncoder接口 * * @author Hanstrovsky */ @Component public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { //加密方法能夠根據本身的須要修改 return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return encode(charSequence).equals(s); } }
這個類是用來存儲登陸成功後的用戶數據,security提供了直接獲取用戶信息的接口json
package com.hanstrovsky.entity; import lombok.Getter; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Collection; /** * 實現UserDetails,可自定義添加更多屬性 * * @author Hanstrovsky */ @Getter @Setter @Component public class MyUserDetails implements UserDetails { //登陸用戶名 private String username; //登陸密碼 private String password; private Collection<? extends GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } private boolean accountNonExpired = true; private boolean accountNonLocked = true; private boolean credentialsNonExpired = true; private boolean enabled = true; }
以上,即可以實現先後端分離項目基本的登陸功能。後端