1. 程式人生 > >在SpringBoot中對SpringSecurity的基本使用

在SpringBoot中對SpringSecurity的基本使用

參考文獻:

Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。

基本使用:

新增依賴:

  1. <!-- 安全框架 Spring Security -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId
    >
  5. </dependency>

我的專案中的使用:

自定義的User物件:

  1. /**
  2. * 自定義的 User 物件
  3. * 此 User 類不是我們的資料庫裡的使用者類,是用來安全服務的
  4. */
  5. public class AnyUser extends User {
  6. //import org.springframework.security.core.userdetails.User;
  7. private Long id;
  8. private String nickname;
  9. AnyUser(
  10. String username,
  11. String password,
  12. Collection<? extends GrantedAuthority> authorities
  13. ) {
  14. super(username, password, authorities);
  15. }
  16. public Long getId() {
  17. return id;
  18. }
  19. public void setId(Long id) {
  20. this.id = id;
  21. }
  22. public String getNickname() {
  23. return nickname;
  24. }
  25. public
    void setNickname(String nickname)
    {
  26. this.nickname = nickname;
  27. }
  28. }
繼承UserDetailsService: 首先這裡我們需要重寫UserDetailsService介面,然後實現該介面中的loadUserByUsername方法,通過該方法查詢到對應的使用者,這裡之所以要實現UserDetailsService介面,是因為在Spring Security中我們配置相關引數需要UserDetailsService型別的資料。 Spring Security 支援把許可權劃分層次,高層次包含低層次的許可權,比如`ROLE_AMDIN,ROLE_USER`兩個許可權,若使用者擁有了ROLE_AMDIN許可權,那麼相當於有了ROLE_USER許可權。使用者被授權了ADMIN,那麼就相當於有其他所有的許可權。
  1. /**
  2. * 自定義 UserDetailsService
  3. */
  4. @Service
  5. class AnyUserDetailsService implements UserDetailsService {
  6. private final UserService userService;
  7. public AnyUserDetailsService(UserService userService){
  8. this.userService = userService;
  9. }
  10. @Override
  11. public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
  12. com.zhou.model.User user = userService.getByEmail(s);
  13. if (user == null){
  14. throw new UsernameNotFoundException("使用者不存在");
  15. }
  16. List<SimpleGrantedAuthority> authorities = new ArrayList<>();
  17. //對應的許可權新增
  18. authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
  19. AnyUser anyUser = new AnyUser(s, user.getPassword(), authorities);
  20. anyUser.setId(user.getId());
  21. anyUser.setNickname(user.getNickname());
  22. return anyUser;
  23. }
  24. }
安全控制中心:
  1. /**
  2. * 安全控制中心
  3. */
  4. @EnableWebSecurity//@EnableWebMvcSecurity 註解開啟Spring Security的功能
  5. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  6. private final UserDetailsService userDetailsService;
  7. public WebSecurityConfig(AnyUserDetailsService userDetailsService){
  8. this.userDetailsService = userDetailsService;
  9. }
  10. @Override
  11. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  12. auth.userDetailsService(this.userDetailsService);
  13. }
  14. /**
  15. * http.authorizeRequests()
  16. .anyRequest().authenticated()
  17. .and().formLogin().loginPage("/login")
  18. //設定預設登入成功跳轉頁面
  19. .defaultSuccessUrl("/index").failureUrl("/login?error").permitAll()
  20. .and()
  21. //開啟cookie儲存使用者資料
  22. .rememberMe()
  23. //設定cookie有效期
  24. .tokenValiditySeconds(60 * 60 * 24 * 7)
  25. //設定cookie的私鑰
  26. .key("")
  27. .and()
  28. .logout()
  29. //預設登出行為為logout,可以通過下面的方式來修改
  30. .logoutUrl("/custom-logout")
  31. //設定登出成功後跳轉頁面,預設是跳轉到登入頁面
  32. .logoutSuccessUrl("")
  33. .permitAll();
  34. * @param http
  35. * @throws Exception
  36. */
  37. @Override
  38. protected void configure(HttpSecurity http) throws Exception {
  39. http
  40. .authorizeRequests()//authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護
  41. .antMatchers("/user/**","/news/**").authenticated()
  42. .anyRequest().permitAll()
  43. .and()
  44. .formLogin()
  45. .loginPage("/login")
  46. .defaultSuccessUrl("/user", true)
  47. .permitAll()
  48. .and()
  49. .logout()
  50. .permitAll()
  51. .and().csrf().disable();
  52. }
  53. }

Spring Security提供了一個過濾器來攔截請求並驗證使用者身份。如果使用者身份認證失敗,頁面就重定向到/login?error,並且頁面中會展現相應的錯誤資訊。若使用者想要登出登入,可以通過訪問@{/logout}請求,在完成登出之後,頁面展現相應的成功訊息。

自定義登入成功處理邏輯:

使登陸成功後跳到登入前頁面:

  1. //處理登入成功的。
  2. @Component("myAuthenticationSuccessHandler")
  3. public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  4. @Autowired
  5. private ObjectMapper objectMapper;
  6. @Override
  7. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
  8. throws IOException, ServletException {
  9. //什麼都不做的話,那就直接呼叫父類的方法
  10. super.onAuthenticationSuccess(request, response, authentication);
  11. String url=request.getRequestURI();
  12. //如果是要跳轉到某個頁面的
  13. new DefaultRedirectStrategy().sendRedirect(request, response, url);
  14. }
  15. }

重新配置安全中心(程式碼完成之後,修改配置config類程式碼。新增2個註解,自動注入):

  1. @Autowired
  2. private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http
  4. .authorizeRequests()//authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護
  5. .antMatchers("/user/**","/news/**","/blog/manage/**","/blog/create/**").authenticated()
  6. .anyRequest().permitAll()
  7. .and()
  8. .formLogin()
  9. .loginPage("/login")
  10. .successHandler(myAuthenticationSuccessHandler)//登陸成功處理
  11. .permitAll()
  12. .and()
  13. .logout()
  14. .permitAll()
  15. .and().csrf().disable();
  16. }

QQ登入實現:

準備工作:為了方便各位測試,這裡直接提供一個可以使用的: APP ID:101386962 APP Key:2a0f820407df400b84a854d054be8b6a 提醒:因為回撥地址不是 http://localhost ,所以在啟動我提供的demo時,需要在host檔案中新增一行:127.0.0.1 www.ictgu.cn

後端詳解:

1、自定義 QQAuthenticationFilter 繼承 AbstractAuthenticationProcessingFilter:
  1. import com.alibaba.fastjson.JSON;
  2. import org.jsoup.Jsoup;
  3. import org.jsoup.nodes.Document;
  4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.core.AuthenticationException;
  7. import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
  8. import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  9. import javax.servlet.ServletException;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import java.io.IOException;
  13. import java.util.regex.Matcher;
  14. import java.util.regex.Pattern;
  15. public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  16. private final static String CODE = "code";
  17. /**
  18. * 獲取 Token 的 API
  19. */
  20. private final static String accessTokenUri = "https://graph.qq.com/oauth2.0/token";
  21. /**
  22. * grant_type 由騰訊提供
  23. */
  24. private final static String grantType = "authorization_code";
  25. /**
  26. * client_id 由騰訊提供
  27. */
  28. public static final String clientId = "101386962";
  29. /**
  30. * client_secret 由騰訊提供
  31. */
  32. private final static String clientSecret = "2a0f820407df400b84a854d054be8b6a";
  33. /**
  34. * redirect_uri 騰訊回撥地址
  35. */
  36. private final static String redirectUri = "http://www.ictgu.cn/login/qq";
  37. /**
  38. * 獲取 OpenID 的 API 地址
  39. */
  40. private final static String openIdUri = "https://graph.qq.com/oauth2.0/me?access_token=";
  41. /**
  42. * 獲取 token 的地址拼接
  43. */
  44. private final static String TOKEN_ACCESS_API = "%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";
  45. public QQAuthenticationFilter(String defaultFilterProcessesUrl) {
  46. super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
  47. }
  48. @Override
  49. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
  50. String code = request.getParameter(CODE);
  51. String tokenAccessApi = String.format(TOKEN_ACCESS_API, accessTokenUri, grantType, clientId, clientSecret, code, redirectUri);
  52. QQToken qqToken = this.getToken(tokenAccessApi);
  53. if (qqToken != null){
  54. String openId = getOpenId(qqToken.getAccessToken());
  55. if (openId != null){
  56. // 生成驗證 authenticationToken
  57. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(qqToken.getAccessToken(), openId);
  58. // 返回驗證結果
  59. return this.getAuthenticationManager().authenticate(authRequest);
  60. }
  61. }
  62. return null;
  63. }
  64. private QQToken getToken(String tokenAccessApi) throws IOException{
  65. Document document = Jsoup.connect(tokenAccessApi).get();
  66. String tokenResult = document.text();
  67. String[] results = tokenResult.split("&");
  68. if (results.length == 3){
  69. QQToken qqToken = new QQToken();
  70. String accessToken = results[0].replace("access_token=", "");
  71. int expiresIn = Integer.valueOf(results[1].replace("expires_in=", ""));
  72. String refreshToken = results[2].replace("refresh_token=", "");
  73. qqToken.setAccessToken(accessToken);
  74. qqToken.setExpiresIn(expiresIn);
  75. qqToken.setRefresh_token(refreshToken);
  76. return qqToken;
  77. }
  78. return null;
  79. }
  80. private String getOpenId(String accessToken) throws IOException{
  81. String url = openIdUri + accessToken;
  82. Document document = Jsoup.connect(url).get();
  83. String resultText = document.text();
  84. Matcher matcher = Pattern.compile("\"openid\":\"(.*?)\"").matcher(resultText);
  85. if (matcher.find()){
  86. return matcher.group(1);
  87. }
  88. return null;
  89. }
  90. class QQToken {
  91. /**
  92. * token
  93. */
  94. private String accessToken;
  95. /**
  96. * 有效期
  97. */
  98. private int expiresIn;
  99. /**
  100. * 重新整理時用的 token
  101. */
  102. private String refresh_token;
  103. String getAccessToken() {
  104. return accessToken;
  105. }
  106. void setAccessToken(String accessToken) {
  107. this.accessToken = accessToken;
  108. }
  109. public int getExpiresIn() {
  110. return expiresIn;
  111. }
  112. void setExpiresIn(int expiresIn) {
  113. this.expiresIn = expiresIn;
  114. }
  115. public String getRefresh_token() {
  116. return refresh_token;
  117. }
  118. void setRefresh_token(String refresh_token) {
  119. this.refresh_token = refresh_token;
  120. }
  121. }
  122. }
說明:Filter 過濾時執行的方法是 doFilter(),由於 QQAuthenticationFilter 繼承了 AbstractAuthenticationProcessingFilter,所以過濾時使用的是父類的doFilter() 方法。 說明:doFilter()方法中,有一步是 attemptAuthentication(request, response) 即為 QQAuthenticationFilter 中實現的方法。這個方法中呼叫了 this.getAuthenticationManager().authenticate(authRequest),這裡自定義了類 QQAuthenticationManager,程式碼如下:
  1. import com.alibaba.fastjson.JSON;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.zhou.model.User;
  4. import org.jsoup.Jsoup;
  5. import org.jsoup.nodes.Document;
  6. import org.springframework.security.authentication.AuthenticationManager;
  7. import org.springframework.security.authentication.BadCredentialsException;
  8. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  9. import org.springframework.security.core.Authentication;
  10. import org.springframework.security.core.AuthenticationException;
  11. import org.springframework.security.core.GrantedAuthority;
  12. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  13. import java.io.IOException;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. import static com.zhou.config.qq.QQAuthenticationFilter.clientId;
  17. public class QQAuthenticationManager implements AuthenticationManager {
  18. private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();
  19. /**
  20. * 獲取 QQ 登入資訊的 API 地址
  21. */
  22. private final static String userInfoUri = "https://graph.qq.com/user/get_user_info";
  23. /**
  24. * 獲取 QQ 使用者資訊的地址拼接
  25. */
  26. private final static String USER_INFO_API = "%s?access_token=%s&oauth_consumer_key=%s&openid=%s";
  27. static {
  28. AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  29. }
  30. @Override
  31. public Authentication authenticate(Authentication auth) throws AuthenticationException {
  32. if (auth.getName() != null && auth.getCredentials() != null) {
  33. User user = null;
  34. try {
  35. user = getUserInfo(auth.getName(), (String) (auth.getCredentials()));
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. }
  39. return new UsernamePasswordAuthenticationToken(user,
  40. null, AUTHORITIES);
  41. }
  42. throw new BadCredentialsException("Bad Credentials");
  43. }
  44. private User getUserInfo(String accessToken, String openId) throws Exception {
  45. String url = String.format(USER_INFO_API, userInfoUri, accessToken, clientId, openId);
  46. Document document;
  47. try {
  48. document = Jsoup.connect(url).get();
  49. } catch (IOException e) {
  50. throw new BadCredentialsException("Bad Credentials!");
  51. }
  52. String resultText = document.text();
  53. JSONObject json = JSON.parseObject(resultText);
  54. User user = new User();
  55. user.setNickname(json.getString("nickname"));
  56. user.setEmail("暫無。。。。");
  57. //user.setGender(json.getString("gender"));
  58. //user.setProvince(json.getString("province"));
  59. //user.setYear(json.getString("year"));
  60. user.setAvatar(json.getString("figureurl_qq_2"));
  61. return user;
  62. }
說明:QQAuthenticationManager 的作用是通過傳來的 token 和 openID 去請求騰訊的getUserInfo介面,獲取騰訊使用者的資訊,並生成新的 Authtication 物件。 接下來就是要將 QQAuthenticationFilter 與 QQAuthenticationManager 結合,配置到 Spring Security 的過濾器鏈中。程式碼如下:
  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http
  4. .authorizeRequests()//authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護
  5. .antMatchers("/user/**","/news/**","/blog/manage/**").authenticated()
  6. .anyRequest().permitAll()
  7. .and()
  8. .formLogin()
  9. .loginPage("/login")
  10. .successHandler(myAuthenticationSuccessHandler)//登陸成功處理
  11. .permitAll()
  12. .and()
  13. .logout()
  14. .permitAll()
  15. .and().csrf().disable();
  16. // 在 UsernamePasswordAuthenticationFilter 前新增 QQAuthenticationFilter
  17. http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
  18. }
  19. /**
  20. * 自定義 QQ登入 過濾器
  21. */
  22. private QQAuthenticationFilter qqAuthenticationFilter(){
  23. QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/login/qq");
  24. //SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
  25. //successHandler.setAlwaysUseDefaultTargetUrl(true);
  26. //successHandler.setDefaultTargetUrl("/user");
  27. MyAuthenticationSuccessHandler successHandler = new MyAuthenticationSuccessHandler();
  28. authenticationFilter.setAuthenticationManager(new QQAuthenticationManager());
  29. authenticationFilter.setAuthenticationSuccessHandler(successHandler);
  30. return authenticationFilter;
  31. }
說明:由於騰訊的回撥地址是 /login/qq,所以 QQAuthenticationFilter 攔截的路徑是 /login/qq,然後將 QQAuthenticationFilter 置於 UsernamePasswordAuthenticationFilter 相同級別的位置。

前端說明:

前端很簡單,一個QQ登陸按鈕,程式碼如下:
<a href="https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101386962&redirect_uri=http://www.ictgu.cn/login/qq&state=test" class="btn btn-primary btn-block">QQ登入</a>
其他說明: 騰訊官網原話:openid是此網站上唯一對應使用者身份的標識,網站可將此ID進行儲存便於使用者下次登入時辨識其身份,或將其與使用者在網站上的原有賬號進行繫結。 通過QQ登入獲取的 openid 用於與自己網站的賬號一一對應。