在SpringBoot中对SpringSecurity的基本使用

Posted 星朝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在SpringBoot中对SpringSecurity的基本使用相关的知识,希望对你有一定的参考价值。

参考文献:

Spring Security Architecture

What is authentication in Spring Security?

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>  

这里有一篇博客入门学习很不错:Spring boot 中 Spring Security 使用改造5部曲

我的项目中的使用:

自定义的User对象:

  1. /** 
  2.  * 自定义的 User 对象 
  3.  * 此 User 类不是我们的数据库里的用户类,是用来安全服务的 
  4.  */  
  5. public class AnyUser extends User {  
  6.     //import org.springframework.security.core.userdetails.User;  
  7.   
  8.     private Long id;  
  9.   
  10.     private String nickname;  
  11.   
  12.     AnyUser(  
  13.             String username,  
  14.             String password,  
  15.             Collection<? extends GrantedAuthority> authorities  
  16.     ) {  
  17.         super(username, password, authorities);  
  18.     }  
  19.   
  20.     public Long getId() {  
  21.         return id;  
  22.     }  
  23.   
  24.     public void setId(Long id) {  
  25.         this.id = id;  
  26.     }  
  27.   
  28.     public String getNickname() {  
  29.         return nickname;  
  30.     }  
  31.   
  32.     public void setNickname(String nickname) {  
  33.         this.nickname = nickname;  
  34.     }  
  35. }  
继承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.   
  7.     private final UserService userService;  
  8.   
  9.     public AnyUserDetailsService(UserService userService){  
  10.         this.userService = userService;  
  11.     }  
  12.   
  13.     @Override  
  14.     public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {  
  15.         com.zhou.model.User user = userService.getByEmail(s);  
  16.         if (user == null){  
  17.             throw new UsernameNotFoundException("用户不存在");  
  18.         }  
  19.         List<SimpleGrantedAuthority> authorities = new ArrayList<>();  
  20.         //对应的权限添加  
  21.         authorities.add(new SimpleGrantedAuthority("ROLE_USER"));  
  22.         AnyUser anyUser = new AnyUser(s, user.getPassword(), authorities);  
  23.         anyUser.setId(user.getId());  
  24.         anyUser.setNickname(user.getNickname());  
  25.         return anyUser;  
  26.     }  
  27.   
  28. }  
安全控制中心:

  1. /** 
  2.  * 安全控制中心 
  3.  */  
  4. @EnableWebSecurity//@EnableWebMvcSecurity 注解开启Spring Security的功能  
  5. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
  6.   
  7.     private final UserDetailsService userDetailsService;  
  8.   
  9.     public WebSecurityConfig(AnyUserDetailsService userDetailsService){  
  10.         this.userDetailsService = userDetailsService;  
  11.     }  
  12.   
  13.     @Override  
  14.     protected void configure(AuthenticationManagerBuilder auth) throws Exception {  
  15.         auth.userDetailsService(this.userDetailsService);  
  16.     }  
  17.   
  18.     /** 
  19.      * http.authorizeRequests() 
  20.      .anyRequest().authenticated() 
  21.      .and().formLogin().loginPage("/login") 
  22.      //设置默认登录成功跳转页面 
  23.      .defaultSuccessUrl("/index").failureUrl("/login?error").permitAll() 
  24.      .and() 
  25.      //开启cookie保存用户数据 
  26.      .rememberMe() 
  27.      //设置cookie有效期 
  28.      .tokenValiditySeconds(60 * 60 * 24 * 7) 
  29.      //设置cookie的私钥 
  30.      .key("") 
  31.      .and() 
  32.      .logout() 
  33.      //默认注销行为为logout,可以通过下面的方式来修改 
  34.      .logoutUrl("/custom-logout") 
  35.      //设置注销成功后跳转页面,默认是跳转到登录页面 
  36.      .logoutSuccessUrl("") 
  37.      .permitAll(); 
  38.      * @param http 
  39.      * @throws Exception 
  40.      */  
  41.     @Override  
  42.     protected void configure(HttpSecurity http) throws Exception {  
  43.         http  
  44.                 .authorizeRequests()//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护  
  45.                 .antMatchers("/user/**","/news/**").authenticated()  
  46.                 .anyRequest().permitAll()  
  47.                 .and()  
  48.                 .formLogin()  
  49.                 .loginPage("/login")  
  50.                 .defaultSuccessUrl("/user"true)  
  51.                 .permitAll()  
  52.                 .and()  
  53.                 .logout()  
  54.                 .permitAll()  
  55.                 .and().csrf().disable();  
  56.     }  
  57.   
  58. }  

Spring Security提供了一个过滤器来拦截请求并验证用户身份。如果用户身份认证失败,页面就重定向到/login?error,并且页面中会展现相应的错误信息。若用户想要注销登录,可以通过访问@{/logout}请求,在完成注销之后,页面展现相应的成功消息。

自定义登录成功处理逻辑:

使登陆成功后跳到登录前页面:

  1. //处理登录成功的。  
  2. @Component("myAuthenticationSuccessHandler")  
  3. public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {  
  4.   
  5.     @Autowired  
  6.     private ObjectMapper objectMapper;  
  7.   
  8.     @Override  
  9.     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)  
  10.             throws IOException, ServletException {  
  11.         //什么都不做的话,那就直接调用父类的方法  
  12.         super.onAuthenticationSuccess(request, response, authentication);  
  13.   
  14.         String url=request.getRequestURI();  
  15.   
  16.         //如果是要跳转到某个页面的  
  17.         new DefaultRedirectStrategy().sendRedirect(request, response, url);  
  18.   
  19.     }  
  20. }  

重新配置安全中心(代码完成之后,修改配置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登录实现:

准备工作:
  • 1、在 QQ互联 申请成为开发者,并创建应用,得到APP ID 和 APP Key。
  • 2、了解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.   
  10. import javax.servlet.ServletException;  
  11. import javax.servlet.http.HttpServletRequest;  
  12. import javax.servlet.http.HttpServletResponse;  
  13. import java.io.IOException;  
  14. import java.util.regex.Matcher;  
  15. import java.util.regex.Pattern;  
  16.   
  17. public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {  
  18.     private final static String CODE = "code";  
  19.   
  20.     /** 
  21.      * 获取 Token 的 API 
  22.      */  
  23.     private final static String accessTokenUri = "https://graph.qq.com/oauth2.0/token";  
  24.   
  25.     /** 
  26.      * grant_type 由腾讯提供 
  27.      */  
  28.     private final static String grantType = "authorization_code";  
  29.   
  30.     /** 
  31.      * client_id 由腾讯提供 
  32.      */  
  33.     public static final String clientId = "101386962";  
  34.   
  35.     /** 
  36.      * client_secret 由腾讯提供 
  37.      */  
  38.     private final static String clientSecret = "2a0f820407df400b84a854d054be8b6a";  
  39.   
  40.     /** 
  41.      * redirect_uri 腾讯回调地址 
  42.      */  
  43.     private final static String redirectUri = "http://www.ictgu.cn/login/qq";  
  44.   
  45.     /** 
  46.      * 获取 OpenID 的 API 地址 
  47.      */  
  48.     private final static String openIdUri = "https://graph.qq.com/oauth2.0/me?access_token=";  
  49.   
  50.     /** 
  51.      * 获取 token 的地址拼接 
  52.      */  
  53.     private final static String TOKEN_ACCESS_API = "%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";  
  54.   
  55.     public QQAuthenticationFilter(String defaultFilterProcessesUrl) {  
  56.         super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));  
  57.     }  
  58.   
  59.     @Override  
  60.     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {  
  61.         String code = request.getParameter(CODE);  
  62.         String tokenAccessApi = String.format(TOKEN_ACCESS_API, accessTokenUri, grantType, clientId, clientSecret, code, redirectUri);  
  63.         QQToken qqToken = this.getToken(tokenAccessApi);  
  64.         if (qqToken != null){  
  65.             String openId = getOpenId(qqToken.getAccessToken());  
  66.             if (openId != null){  
  67.                 // 生成验证 authenticationToken  
  68.                 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(qqToken.getAccessToken(), openId);  
  69.                 // 返回验证结果  
  70.                 return this.getAuthenticationManager().authenticate(authRequest);  
  71.             }  
  72.         }  
  73.         return null;  
  74.     }  
  75.   
  76.     private QQToken getToken(String tokenAccessApi) throws IOException{  
  77.         Document document = Jsoup.connect(tokenAccessApi).get();  
  78.         String tokenResult = document.text();  
  79.         String[] results = tokenResult.split("&");  
  80.         if (results.length == 3){  
  81.             QQToken qqToken = new QQToken();  
  82.             String accessToken = results[0].replace("access_token=""");  
  83.             int expiresIn = Integer.valueOf(results[1].replace("expires_in="""));  
  84.             String refreshToken = results[2].replace("refresh_token=""");  
  85.             qqToken.setAccessToken(accessToken);  
  86.             qqToken.setExpiresIn(expiresIn);  
  87.             qqToken.setRefresh_token(refreshToken);  
  88.             return qqToken;  
  89.         }  
  90.         return null;  
  91.     }  
  92.   
  93.     private String getOpenId(String accessToken) throws IOException{  
  94.         String url = openIdUri + accessToken;  
  95.         Document document = Jsoup.connect(url).get();  
  96.         String resultText = document.text();  
  97.         Matcher matcher = Pattern.compile("\"openid\":\"(.*?)\"").matcher(resultText);  
  98.         if (matcher.find()){  
  99.             return matcher.group(1);  
  100.         }  
  101.         return null;  
  102.     }  
  103.   
  104.     class QQToken {  
  105.   
  106.         /** 
  107.          * token 
  108.          */  
  109.         private String accessToken;  
  110.   
  111.         /** 
  112.          * 有效期 
  113.          */  
  114.         private int expiresIn;  
  115.   
  116.         /** 
  117.          * 刷新时用的 token 
  118.          */  
  119.         private String refresh_token;  
  120.   
  121.         String getAccessToken() {  
  122.             return accessToken;  
  123.         }  
  124.   
  125.         void setAccessToken(String accessToken) {  
  126.             this.accessToken = accessToken;  
  127.         }  
  128.   
  129.         public int getExpiresIn() {  
  130.             return expiresIn;  
  131.         }  
  132.   
  133.         void setExpiresIn(int expiresIn) {  
  134.             this.expiresIn = expiresIn;  
  135.         }  
  136.   
  137.         public String getRefresh_token() {  
  138.             return refresh_token;  
  139.         }  
  140.   
  141.         void setRefresh_token(String refresh_token) {  
  142.             this.refresh_token = refresh_token;  
  143.         }  
  144.     }  
  145. }  
说明: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.   
  17. import static com.zhou.config.qq.QQAuthenticationFilter.clientId;  
  18.   
  19. public class QQAuthenticationManager implements AuthenticationManager {  
  20.     private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();  
  21.   
  22.     /** 
  23.      * 获取 QQ 登录信息的 API 地址 
  24.      */  
  25.     private final static String userInfoUri = "https://graph.qq.com/user/get_user_info";  
  26.   
  27.     /** 
  28.      * 获取 QQ 用户信息的地址拼接 
  29.      */  
  30.     private final static String USER_INFO_API = "%s?access_token=%s&oauth_consumer_key=%s&openid=%s";  
  31.   
  32.     static {  
  33.         AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));  
  34.     }  
  35.   
  36.     @Override  
  37.     public Authentication authenticate(Authentication auth) throws AuthenticationException {  
  38.         if (auth.getName() != null && auth.getCredentials() != null) {  
  39.             User user = null;  
  40.             try {  
  41.                 user = getUserInfo(auth.getName(), (String) (auth.getCredentials()));  
  42.             } catch (Exception e) {  
  43.                 e.printStackTrace();  
  44.             }  
  45.             return new UsernamePasswordAuthenticationToken(user,  
  46.                     null, AUTHORITIES);  
  47.         }  
  48.         throw new BadCredentialsException("Bad Credentials");  
  49.     }  
  50.   
  51.     private User getUserInfo(String accessToken, String openId) throws Exception {  
  52.         String url = String.format(USER_INFO_API, userInfoUri, accessToken, clientId, openId);  
  53.         Document document;  
  54.         try {  
  55.             document = Jsoup.connect(url).get();  
  56.         } catch (IOException e) {  
  57.             throw new BadCredentialsException("Bad Credentials!");  
  58.         }  
  59.         String resultText = document.text();  
  60.         JSONObject json = JSON.parseObject(resultText);  
  61.   
  62.         User user = new User();  
  63.         user.setNickname(json.getString("nickname"));  
  64.         user.setEmail("暂无。。。。");  
  65.         //user.setGender(json.getString("gender"));  
  66.         //user.setProvince(json.getString("province"));  
  67.         //user.setYear(json.getString("year"));  
  68.         user.setAvatar(json.getString("figureurl_qq_2"));  
  69.   
  70.         return user;  
  71.     }  
说明: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.     /**  
  21.      * 自定义 QQ登录 过滤器  
  22.      */  
  23.     private QQAuthenticationFilter qqAuthenticationFilter(){  
  24.         QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/login/qq");  
  25.         //SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();  
  26.         //successHandler.setAlwaysUseDefaultTargetUrl(true);  
  27.         //successHandler.setDefaultTargetUrl("/user");  
  28.         MyAuthenticationSuccessHandler successHandler = new MyAuthenticationSuccessHandler();  
  29.         authenticationFilter.setAuthenticationManager(new QQAuthenticationManager());  
  30.         authenticationFilter.setAuthenticationSuccessHandler(successHandler);  
  31.         return authenticationFilter;  
  32.     }  
说明:由于腾讯的回调地址是 /login/qq,所以 QQAuthenticationFilter 拦截的路径是 /login/qq,然后将 QQAuthenticationFilter 置于 UsernamePasswordAuthenticationFilter 相同级别的位置。

前端说明:

前端很简单,一个QQ登陆按钮,代码如下:
  1. <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 用于与自己网站的账号一一对应。

























以上是关于在SpringBoot中对SpringSecurity的基本使用的主要内容,如果未能解决你的问题,请参考以下文章

使用springboot简单整合springsecurity和mybatis-plus

在SpringBoot中对SpringSecurity的基本使用

Springboot中对Service层进行集成测试时注意点

简单实现SpringBoot中对Apollo配置的动态监听

验证 Auth0 令牌 - Spring Security

JSESSIONID 或 X-AUTH-TOKEN 在 Spring boot 中对同一用户进行身份验证时重复