Spring boot 中 Spring Security 使用改造5部曲(转)

文章的内容有点长,也是自己学习Spring security的一个总结。如果你从头看到尾,我想你对Spring Security的使用和基本原理应该会有一个比较清晰的认识。
如果有什么理解不对的地方,请留言,谢谢。
 
一、Spring security 是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
二、Spring security 怎么使用?
使用Spring Security很简单,只要在pom.xml文件中,引入spring security的依赖就可以了。
            <!-- spring security依赖 -->
            <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
什么都不做,直接运行程序,这时你访问任何一个URL,都会弹出一个“需要授权”的验证框,如图:

,spring security 会默认使用一个用户名为:user 的用户,密码就是 启动的时候生成的(通过控制台console中查看),如图

然后在用户名中输入:user   密码框中输入 上面的密码 ,之后就可以正常访问之前URL了。很显然这根本不是我们想要的,接下来我们需要一步一步的改造。

 改造1 使用页面表单登录

首先 添加一个类 SecurityConfig 继承  WebSecurityConfigurerAdapter ,
重写 configure 方法。
并加上@Configuration 和@EnableWebSecurity 2个注解。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage(
"/login").loginProcessingUrl("/login/form").failureUrl("/login-error").permitAll() //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
}
}
View Code
 loginPage("/login")表示登录时跳转的页面,因为登录页面我们不需要登录认证,所以我们需要添加 permitAll() 方法。
 
  添加一个控制器,对应/login 返回一个登录页面。
  @RequestMapping ( "/login" )
  public String userLogin ()
  {
      
        return "demo-sign" ;
  }
 html页面是使用 thymeleaf 模板引擎的,这里就不详细讲解了。
 
demo_sign.html 的 html部分代码如下:  
复制代码
<form  class="form-signin" action="/login/form" method="post">
<h2 class="form-signin-heading">用户登录</h2>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" class="form-control" placeholder="请输入用户名"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" class="form-control" placeholder="请输入密码" /></td> </tr>
<tr>

<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登录</button>
</td>
</tr>
</table>
</form>
复制代码
需要注意下: form提交的url要和配置文件中的 loginProcessingUrl("")中的一致。
failureUrl=表示登录出错的页面,我们可以简单写个提示:如 用户名或密码错误。
  @RequestMapping ( "/login-error" )
  public String loginError ()
  {
        return "login-error" ;
       
  }
login-error.html
复制代码
<!DOCTYPE HTML><html xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org"><head><title>用户登录</title><link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" /><link rel="stylesheet" href="/css/sign.css" /></head><body>            <h3>用户名或密码错误</h3></body></html>
复制代码
 运行程序:如果输入错误的用户名和密码的话,则会显示如下图所示:

我们用一个测试的RestController来测试

复制代码
@RestControllerpublic class HelloWorldController {      @RequestMapping("/hello")      public String helloWorld()      {            return "spring security hello world";      }}
复制代码
当没有登录时,输入 http://localhost:port/hello 时,则直接跳转到我们登录页面,登录成功之后,再访问 时,就能显示我们期望的值了。
改造2、自定义用户名和密码
很显然,这样改造之后,虽然登录页面是好看了,但还远远不能满足我们的应用需求,所以第二步,我们改造自定义的用户名和密码。
自定义用户名和密码有2种方式,一种是在代码中写死,这也是官方的demo,另一种是使用数据库
首先是第一种:如
复制代码
@Autowired        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {                auth                        .inMemoryAuthentication()                                .withUser("user").password("password").roles("USER");        }
复制代码

我们也照样,这是把用户名改成 admin 密码改成 123456   roles是该用户的角色,我们后面再细说。

复制代码
      @Autowired      public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {            auth                  .inMemoryAuthentication()                        .withUser("admin").password("123456").roles("USER");                  }
复制代码

还有种方法 就是 重写 另外一种configure(AuthenticationManagerBuilder auth) 方法,这个和上面那个方法的作用是一样的。选其一就可。

复制代码
 @Override      protected void configure(AuthenticationManagerBuilder auth) throws Exception {            // TODO Auto-generated method stub                        auth            .inMemoryAuthentication()                  .withUser("admin").password("123456").roles("USER")                  .and()                  .withUser("test").password("test123").roles("ADMIN");      }
复制代码

程序运行起来,这时用我们自己的用户名和密码 输入 admin 和123456 就可以了。

你也可以多几个用户,就多几个withUser即可。
. and () . withUser ( "test" ) . password ( "test123" ) . roles ( "ADMIN" ) ;  这样我们就有了一个用户名为test,密码为test123的用户了。
第一种的只是让我们体验了一下Spring Security而已,我们接下来就要提供自定义的用户认证机制及处理过程。
在讲这个之前,我们需要知道spring security的原理,spring security的原理就是使用很多的拦截器对URL进行拦截,以此来管理登录验证和用户权限验证。
 
用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
 
所以我们要自定义用户的校验机制的话,我们只要实现自己的AuthenticationProvider就可以了。在用AuthenticationProvider 这个之前,我们需要提供一个获取用户信息的服务,实现   UserDetailsService 接口
用户名密码->(Authentication(未认证)  ->  AuthenticationManager ->AuthenticationProvider->UserDetailService->UserDetails->Authentication(已认证)
了解了这个原理之后,我们就开始写代码
第一步:我们定义自己的用户信息类 UserInfo 继承UserDetails和Serializable接口
代码如下:
public class UserInfo implements Serializable, UserDetails {      /**       *       */      private static final long serialVersionUID = 1L;      private String username;      private String password;      private String role;      private boolean accountNonExpired;      private boolean accountNonLocked;      private boolean credentialsNonExpired;      private boolean enabled;      public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked,                  boolean credentialsNonExpired, boolean enabled) {            // TODO Auto-generated constructor stub            this.username = username;            this.password = password;            this.role = role;            this.accountNonExpired = accountNonExpired;            this.accountNonLocked = accountNonLocked;            this.credentialsNonExpired = credentialsNonExpired;            this.enabled = enabled;      }      // 这是权限      @Override      public Collection<? extends GrantedAuthority> getAuthorities() {            // TODO Auto-generated method stub            return AuthorityUtils.commaSeparatedStringToAuthorityList(role);      }      @Override      public String getPassword() {            // TODO Auto-generated method stub            return password;      }      @Override      public String getUsername() {            // TODO Auto-generated method stub            return username;      }      @Override      public boolean isAccountNonExpired() {            // TODO Auto-generated method stub            return accountNonExpired;      }      @Override      public boolean isAccountNonLocked() {            // TODO Auto-generated method stub            return accountNonLocked;      }      @Override      public boolean isCredentialsNonExpired() {            // TODO Auto-generated method stub            return credentialsNonExpired;      }      @Override      public boolean isEnabled() {            // TODO Auto-generated method stub            return enabled;      }}
View Code

然后实现第2个类 UserService 来返回这个UserInfo的对象实例

@Componentpublic class MyUserDetailsService implements UserDetailsService {              @Override      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {            // TODO Auto-generated method stub            //这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。            return null;      }}            // TODO Auto-generated method stub                                    //这里可以通过数据库来查找到实际的用户信息,这里我们先模拟下,后续我们用数据库来实现            if(username.equals("admin"))            {                  //假设返回的用户信息如下;                  UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);                  return userInfo;                                          }                        return null;
View Code
到这里为止,我们自己定义的UserInfo类和从数据库中返回具体的用户信息已经实现,接下来我们要实现的,我们自己的 AuthenticationProvider
新建类 MyAuthenticationProvider 继承AuthenticationProvider
完整的代码如下:
@Componentpublic class MyAuthenticationProvider implements AuthenticationProvider {      /**       * 注入我们自己定义的用户信息获取对象       */      @Autowired      private UserDetailsService userDetailService;      @Override      public Authentication authenticate(Authentication authentication) throws AuthenticationException {            // TODO Auto-generated method stub            String userName = authentication.getName();// 这个获取表单输入中返回的用户名;            String password = (String) authentication.getPrincipal();// 这个是表单中输入的密码;            // 这里构建来判断用户是否存在和密码是否正确            UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 这里调用我们的自己写的获取用户的方法;            if (userInfo == null) {                  throw new BadCredentialsException("用户名不存在");            }            // //这里我们还要判断密码是否正确,实际应用中,我们的密码一般都会加密,以Md5加密为例            // Md5PasswordEncoder md5PasswordEncoder=new Md5PasswordEncoder();            // //这里第个参数,是salt            // 就是加点盐的意思,这样的好处就是用户的密码如果都是123456,由于盐的不同,密码也是不一样的,就不用怕相同密码泄漏之后,不会批量被破解。            // String encodePwd=md5PasswordEncoder.encodePassword(password, userName);            // //这里判断密码正确与否            // if(!userInfo.getPassword().equals(encodePwd))            // {            // throw new BadCredentialsException("密码不正确");            // }            // //这里还可以加一些其他信息的判断,比如用户账号已停用等判断,这里为了方便我接下去的判断,我就不用加密了。            //            //            if (!userInfo.getPassword().equals("123456")) {                  throw new BadCredentialsException("密码不正确");            }            Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();            // 构建返回的用户登录成功的token            return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);      }      @Override      public boolean supports(Class<?> authentication) {            // TODO Auto-generated method stub            // 这里直接改成retrun true;表示是支持这个执行            return true;      }}
View Code

到此为止,我们的用户信息的获取,校验部分已经完成了。接下来要让它起作用,则我们需要在配置文件中修改,让他起作用。回到我的SecurityConfig代码文件,修改如下:

1、注入我们自己的 AuthenticationProvider
2、修改配置的方法:
    @Autowired    private AuthenticationProvider provider;  //注入我们自己的AuthenticationProvider    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        // TODO Auto-generated method stub        auth.authenticationProvider(provider);//        auth//        .inMemoryAuthentication()//            .withUser("admin").password("123456").roles("USER")//            .and()//            .withUser("test").password("test123").roles("ADMIN");    }
View Code
现在重新运行程序,则需要输入用户名为 admin 密码是123456之后,才能正常登录了。
为了方便测试,我们调整添加另一个控制器 /whoim 的代码 ,让他返回当前登录的用户信息,前面说了,他是存在SecurityContextHolder 的全局变量中,所以我们可以这样获取
       @RequestMapping("/whoim")      public Object whoIm()      {            return SecurityContextHolder.getContext().getAuthentication().getPrincipal();      }
View Code

我们运行,直接反问 /whoim ,则直接跳转到登录页面,我们验证过之后,再访问此url,结果如下:

到这里,我们自定义的登录已经成功了。

改造3、自定义登录成功和失败的处理逻辑

在现在的大多数应用中,一般都是前后端分离的,所以我们登录成功或失败都需要用json格式返回,或者登录成功之后,跳转到某个具体的页面。
接下来我们来实现这种改造。
 
为了实现这个功能,我们需要写2个类,分别继承 SavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandler2个类,并重写其中的部分方法即可。
//处理登录成功的。@Component("myAuthenticationSuccessHandler")public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{            @Autowired      private ObjectMapper objectMapper;      @Override      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)                  throws IOException, ServletException {                        //什么都不做的话,那就直接调用父类的方法            super.onAuthenticationSuccess(request, response, authentication);                          //这里可以根据实际情况,来确定是跳转到页面或者json格式。            //如果是返回json格式,那么我们这么写                        Map<String,String> map=new HashMap<>();            map.put("code", "200");            map.put("msg", "登录成功");            response.setContentType("application/json;charset=UTF-8");            response.getWriter().write(objectMapper.writeValueAsString(map));                                    //如果是要跳转到某个页面的,比如我们的那个whoim的则            new DefaultRedirectStrategy().sendRedirect(request, response, "/whoim");                  }}
View Code
//登录失败的@Component("myAuthenticationFailHander")public class MyAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {      @Autowired      private ObjectMapper objectMapper;      private Logger logger = LoggerFactory.getLogger(getClass());      @Override      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,                  AuthenticationException exception) throws IOException, ServletException {            // TODO Auto-generated method stub            logger.info("登录失败");            //以Json格式返回            Map<String,String> map=new HashMap<>();            map.put("code", "201");            map.put("msg", "登录失败");            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());            response.setContentType("application/json");            response.setCharacterEncoding("UTF-8");               response.getWriter().write(objectMapper.writeValueAsString(map));                  }}
View Code
代码完成之后,修改配置config类代码。
添加2个注解,自动注入
      @Autowired      private AuthenticationSuccessHandler myAuthenticationSuccessHandler;      @Autowired      private AuthenticationFailureHandler myAuthenticationFailHander;            @Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests().anyRequest().authenticated()                                    .and()                  .csrf().disable();                  }
View Code

进行测试,我们先返回json格式的(登录成功和失败的)

   

改成跳转到默认页面

改造4、添加权限控制

之前的代码我们用户的权限没有加以利用,现在我们添加权限的用法。
之前的登录验证通俗的说,就是来判断你是谁(认证),而权限控制就是用来确定:你能做什么或者不能做什么(权限)
 
在讲这个之前,我们简单说下,对于一些资源不需要权限认证的,那么就可以在Config中添加 过滤条件,如:
@Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()                        .antMatchers("/index").permitAll()  //这就表示 /index这个页面不需要权限认证,所有人都可以访问                  .anyRequest().authenticated()                               .and()                  .csrf().disable();                  }
View Code
那么我们直接访问 /index 就不会跳转到登录页面,这样我们就可以把一些不需要验证的资源以这种方式过滤,比如图片,脚本,样式文件之类的。
我们先来看第一种:在编码中写死的。
那其实权限控制也是通过这种方式来实现:
   http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()                        .antMatchers("/index").permitAll()                                      .antMatchers("/whoim").hasRole("ADMIN") //这就表示/whoim的这个资源需要有ROLE_ADMIN的这个角色才能访问。不然就会提示拒绝访问                  .anyRequest().authenticated() //必须经过认证以后才能访问                            .and()                  .csrf().disable();   
View Code

这个用户的角色哪里来,就是我们自己的UserDetailsService中返回的用户信息中的角色权限信息,这里需要注意一下就是 .hasRole("ADMIN"),那么给用户的角色时就要用:ROLE_ADMIN 

.antMatchers 这里也可以限定HttpMethod的不同要求不同的权限(用于适用于Restful风格的API).
如:Post需要 管理员权限,get 需要user权限,我们可以这么个改造,同时也可以通过通配符来是实现 如:/user/1 这种带参数的URL
. antMatchers ( "/whoim" ) . hasRole ( "ADMIN" )
       . antMatchers ( HttpMethod . POST , "/user/*" ) . hasRole ( "ADMIN" )
      . antMatchers ( HttpMethod . GET , "/user/*" ) . hasRole ( "USER" )
 
Spring Security 的校验的原理:左手配置信息,右手登录后的用户信息,中间投票器。
 从我们的配置信息中获取相关的URL和需要的权限信息,然后获得登录后的用户信息,
然后经过:AccessDecisionManager 来验证,这里面有多个投票器:AccessDecisionVoter,(默认有几种实现:比如:1票否决(只要有一个不同意,就没有权限),全票通过,才算通过;只要有1个通过,就全部通过。类似这种的。
WebExpressionVoter 是Spring Security默认提供的的web开发的投票器。(表达式的投票器
 
Spring Security 默认的是 AffirmativeBased   只要有一个通过,就通过。
有兴趣的可以 从FilterSecurityInterceptor这个过滤器入口,来查看这个流程。
内嵌的表达式有:permitAll  denyAll   等等。
每一个权限表达式都对应一个方法。
如果需要同时满足多个要求的,不能连写如 ,我们有个URL需要管理员权限也同时要限定IP的话,不能: . hasRole ( "ADMIN" ).hasIPAddress("192.168.1.1"); 
而是需要用access方法    .access("hasRole('ADMIN') and hasIpAddress('192.168.1.1')");这种。
 
那我们可以自己写权限表达式吗? 可以,稍后。。。这些都是硬编码的实现,都是在代码中写入的,这样的灵活性不够。所以我们接下来继续改造
改造4、添加基于RBAC(role-Based-access control)权限控制
这个大家可以去百度一下,一般都是由 3个部分组成,一个是用户,一个是角色 ,一个是资源(菜单,按钮),然后就是 用户和角色的关联表,角色和资源的关联表
 
核心就是判断当前的用户所拥有的URL是否和当前访问的URL是否匹配。
首先我们自己提供一个判断的接口和实现,代码如下:
/** * 返回权限验证的接口 *  * */public interface RbacService {      boolean hasPermission(HttpServletRequest request,Authentication authentication);}@Component("rbacService")public class RbacServiceImpl implements RbacService {      private AntPathMatcher antPathMatcher = new AntPathMatcher();      @Override      public boolean hasPermission(HttpServletRequest request, Authentication authentication) {            Object principal = authentication.getPrincipal();            boolean hasPermission = false;            if (principal instanceof UserDetails) { //首先判断先当前用户是否是我们UserDetails对象。                  String userName = ((UserDetails) principal).getUsername();                  Set<String> urls = new HashSet<>(); // 数据库读取 //读取用户所拥有权限的所有URL                                    urls.add("/whoim");                  // 注意这里不能用equal来判断,因为有些URL是有参数的,所以要用AntPathMatcher来比较                  for (String url : urls) {                        if (antPathMatcher.match(url, request.getRequestURI())) {                              hasPermission = true;                              break;                        }                  }            }            return hasPermission;      }}
View Code

然后在Security的配置项中添加自定义的权限表达式就可以了。

@Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()//                      .antMatchers("/index").permitAll()                    //                .antMatchers("/whoim").hasRole("ADMIN")//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必须经过认证以后才能访问                              .and()                  .csrf().disable();                  }
View Code

其中 @rbacService 就是我们自己声明的bean,在RbacServiceImpl实现类的头部注解中。

改造5、记住我的功能Remeber me

本质是通过token来读取用户信息,所以服务端需要存储下token信息
根据官方的文档,token可以通过数据库存储 数据库脚本
复制代码
CREATE TABLE persistent_logins (    username VARCHAR(64) NOT NULL,    series VARCHAR(64) NOT NULL,    token VARCHAR(64) NOT NULL,    last_used TIMESTAMP NOT NULL,    PRIMARY KEY (series));
复制代码

然后,配置好token 的存储 及数据源

 @Autowired      private DataSource dataSource;   //是在application.properites      /**       * 记住我功能的token存取器配置       * @return       */      @Bean      public PersistentTokenRepository persistentTokenRepository() {            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();            tokenRepository.setDataSource(dataSource);            return tokenRepository;      }
View Code

修改Security配置

  @Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .rememberMe()                        .rememberMeParameter("remember-me").userDetailsService(userDetailsService)                        .tokenRepository(persistentTokenRepository())                        .tokenValiditySeconds(60)                  .and()                  .authorizeRequests()//                      .antMatchers("/index").permitAll()                    //                .antMatchers("/whoim").hasRole("ADMIN")//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必须经过认证以后才能访问                              .and()                  .csrf().disable();      
View Code
登录之后 数据库就会有一条数据
然后,服务重新启动下,我们在看下直接访问 /whoim 的话,就可以直接访问了,不需要再登录了。
 
到此为止我们的Spring Securtiy 的基本用法已经改造完成了。
接下来,我会继续学习下Spring Security Oauth2 的内容,敬请期待。

原文地址:http://www.cnblogs.com/SmallTalker/p/7851848.html

文章的内容有点长,也是自己学习Spring security的一个总结。如果你从头看到尾,我想你对Spring Security的使用和基本原理应该会有一个比较清晰的认识。
如果有什么理解不对的地方,请留言,谢谢。
 
一、Spring security 是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
二、Spring security 怎么使用?
使用Spring Security很简单,只要在pom.xml文件中,引入spring security的依赖就可以了。
            <!-- spring security依赖 -->
            <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
什么都不做,直接运行程序,这时你访问任何一个URL,都会弹出一个“需要授权”的验证框,如图:

,spring security 会默认使用一个用户名为:user 的用户,密码就是 启动的时候生成的(通过控制台console中查看),如图

然后在用户名中输入:user   密码框中输入 上面的密码 ,之后就可以正常访问之前URL了。很显然这根本不是我们想要的,接下来我们需要一步一步的改造。

 改造1 使用页面表单登录

首先 添加一个类 SecurityConfig 继承  WebSecurityConfigurerAdapter ,
重写 configure 方法。
并加上@Configuration 和@EnableWebSecurity 2个注解。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage(
"/login").loginProcessingUrl("/login/form").failureUrl("/login-error").permitAll() //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
}
}
View Code
 loginPage("/login")表示登录时跳转的页面,因为登录页面我们不需要登录认证,所以我们需要添加 permitAll() 方法。
 
  添加一个控制器,对应/login 返回一个登录页面。
  @RequestMapping ( "/login" )
  public String userLogin ()
  {
      
        return "demo-sign" ;
  }
 html页面是使用 thymeleaf 模板引擎的,这里就不详细讲解了。
 
demo_sign.html 的 html部分代码如下:  
复制代码
<form  class="form-signin" action="/login/form" method="post">
<h2 class="form-signin-heading">用户登录</h2>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" class="form-control" placeholder="请输入用户名"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" class="form-control" placeholder="请输入密码" /></td> </tr>
<tr>

<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登录</button>
</td>
</tr>
</table>
</form>
复制代码
需要注意下: form提交的url要和配置文件中的 loginProcessingUrl("")中的一致。
failureUrl=表示登录出错的页面,我们可以简单写个提示:如 用户名或密码错误。
  @RequestMapping ( "/login-error" )
  public String loginError ()
  {
        return "login-error" ;
       
  }
login-error.html
复制代码
<!DOCTYPE HTML><html xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org"><head><title>用户登录</title><link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" /><link rel="stylesheet" href="/css/sign.css" /></head><body>            <h3>用户名或密码错误</h3></body></html>
复制代码
 运行程序:如果输入错误的用户名和密码的话,则会显示如下图所示:

我们用一个测试的RestController来测试

复制代码
@RestControllerpublic class HelloWorldController {      @RequestMapping("/hello")      public String helloWorld()      {            return "spring security hello world";      }}
复制代码
当没有登录时,输入 http://localhost:port/hello 时,则直接跳转到我们登录页面,登录成功之后,再访问 时,就能显示我们期望的值了。
改造2、自定义用户名和密码
很显然,这样改造之后,虽然登录页面是好看了,但还远远不能满足我们的应用需求,所以第二步,我们改造自定义的用户名和密码。
自定义用户名和密码有2种方式,一种是在代码中写死,这也是官方的demo,另一种是使用数据库
首先是第一种:如
复制代码
@Autowired        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {                auth                        .inMemoryAuthentication()                                .withUser("user").password("password").roles("USER");        }
复制代码

我们也照样,这是把用户名改成 admin 密码改成 123456   roles是该用户的角色,我们后面再细说。

复制代码
      @Autowired      public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {            auth                  .inMemoryAuthentication()                        .withUser("admin").password("123456").roles("USER");                  }
复制代码

还有种方法 就是 重写 另外一种configure(AuthenticationManagerBuilder auth) 方法,这个和上面那个方法的作用是一样的。选其一就可。

复制代码
 @Override      protected void configure(AuthenticationManagerBuilder auth) throws Exception {            // TODO Auto-generated method stub                        auth            .inMemoryAuthentication()                  .withUser("admin").password("123456").roles("USER")                  .and()                  .withUser("test").password("test123").roles("ADMIN");      }
复制代码

程序运行起来,这时用我们自己的用户名和密码 输入 admin 和123456 就可以了。

你也可以多几个用户,就多几个withUser即可。
. and () . withUser ( "test" ) . password ( "test123" ) . roles ( "ADMIN" ) ;  这样我们就有了一个用户名为test,密码为test123的用户了。
第一种的只是让我们体验了一下Spring Security而已,我们接下来就要提供自定义的用户认证机制及处理过程。
在讲这个之前,我们需要知道spring security的原理,spring security的原理就是使用很多的拦截器对URL进行拦截,以此来管理登录验证和用户权限验证。
 
用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
 
所以我们要自定义用户的校验机制的话,我们只要实现自己的AuthenticationProvider就可以了。在用AuthenticationProvider 这个之前,我们需要提供一个获取用户信息的服务,实现   UserDetailsService 接口
用户名密码->(Authentication(未认证)  ->  AuthenticationManager ->AuthenticationProvider->UserDetailService->UserDetails->Authentication(已认证)
了解了这个原理之后,我们就开始写代码
第一步:我们定义自己的用户信息类 UserInfo 继承UserDetails和Serializable接口
代码如下:
public class UserInfo implements Serializable, UserDetails {      /**       *       */      private static final long serialVersionUID = 1L;      private String username;      private String password;      private String role;      private boolean accountNonExpired;      private boolean accountNonLocked;      private boolean credentialsNonExpired;      private boolean enabled;      public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked,                  boolean credentialsNonExpired, boolean enabled) {            // TODO Auto-generated constructor stub            this.username = username;            this.password = password;            this.role = role;            this.accountNonExpired = accountNonExpired;            this.accountNonLocked = accountNonLocked;            this.credentialsNonExpired = credentialsNonExpired;            this.enabled = enabled;      }      // 这是权限      @Override      public Collection<? extends GrantedAuthority> getAuthorities() {            // TODO Auto-generated method stub            return AuthorityUtils.commaSeparatedStringToAuthorityList(role);      }      @Override      public String getPassword() {            // TODO Auto-generated method stub            return password;      }      @Override      public String getUsername() {            // TODO Auto-generated method stub            return username;      }      @Override      public boolean isAccountNonExpired() {            // TODO Auto-generated method stub            return accountNonExpired;      }      @Override      public boolean isAccountNonLocked() {            // TODO Auto-generated method stub            return accountNonLocked;      }      @Override      public boolean isCredentialsNonExpired() {            // TODO Auto-generated method stub            return credentialsNonExpired;      }      @Override      public boolean isEnabled() {            // TODO Auto-generated method stub            return enabled;      }}
View Code

然后实现第2个类 UserService 来返回这个UserInfo的对象实例

@Componentpublic class MyUserDetailsService implements UserDetailsService {              @Override      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {            // TODO Auto-generated method stub            //这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。            return null;      }}            // TODO Auto-generated method stub                                    //这里可以通过数据库来查找到实际的用户信息,这里我们先模拟下,后续我们用数据库来实现            if(username.equals("admin"))            {                  //假设返回的用户信息如下;                  UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);                  return userInfo;                                          }                        return null;
View Code
到这里为止,我们自己定义的UserInfo类和从数据库中返回具体的用户信息已经实现,接下来我们要实现的,我们自己的 AuthenticationProvider
新建类 MyAuthenticationProvider 继承AuthenticationProvider
完整的代码如下:
@Componentpublic class MyAuthenticationProvider implements AuthenticationProvider {      /**       * 注入我们自己定义的用户信息获取对象       */      @Autowired      private UserDetailsService userDetailService;      @Override      public Authentication authenticate(Authentication authentication) throws AuthenticationException {            // TODO Auto-generated method stub            String userName = authentication.getName();// 这个获取表单输入中返回的用户名;            String password = (String) authentication.getPrincipal();// 这个是表单中输入的密码;            // 这里构建来判断用户是否存在和密码是否正确            UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 这里调用我们的自己写的获取用户的方法;            if (userInfo == null) {                  throw new BadCredentialsException("用户名不存在");            }            // //这里我们还要判断密码是否正确,实际应用中,我们的密码一般都会加密,以Md5加密为例            // Md5PasswordEncoder md5PasswordEncoder=new Md5PasswordEncoder();            // //这里第个参数,是salt            // 就是加点盐的意思,这样的好处就是用户的密码如果都是123456,由于盐的不同,密码也是不一样的,就不用怕相同密码泄漏之后,不会批量被破解。            // String encodePwd=md5PasswordEncoder.encodePassword(password, userName);            // //这里判断密码正确与否            // if(!userInfo.getPassword().equals(encodePwd))            // {            // throw new BadCredentialsException("密码不正确");            // }            // //这里还可以加一些其他信息的判断,比如用户账号已停用等判断,这里为了方便我接下去的判断,我就不用加密了。            //            //            if (!userInfo.getPassword().equals("123456")) {                  throw new BadCredentialsException("密码不正确");            }            Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();            // 构建返回的用户登录成功的token            return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);      }      @Override      public boolean supports(Class<?> authentication) {            // TODO Auto-generated method stub            // 这里直接改成retrun true;表示是支持这个执行            return true;      }}
View Code

到此为止,我们的用户信息的获取,校验部分已经完成了。接下来要让它起作用,则我们需要在配置文件中修改,让他起作用。回到我的SecurityConfig代码文件,修改如下:

1、注入我们自己的 AuthenticationProvider
2、修改配置的方法:
    @Autowired    private AuthenticationProvider provider;  //注入我们自己的AuthenticationProvider    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        // TODO Auto-generated method stub        auth.authenticationProvider(provider);//        auth//        .inMemoryAuthentication()//            .withUser("admin").password("123456").roles("USER")//            .and()//            .withUser("test").password("test123").roles("ADMIN");    }
View Code
现在重新运行程序,则需要输入用户名为 admin 密码是123456之后,才能正常登录了。
为了方便测试,我们调整添加另一个控制器 /whoim 的代码 ,让他返回当前登录的用户信息,前面说了,他是存在SecurityContextHolder 的全局变量中,所以我们可以这样获取
       @RequestMapping("/whoim")      public Object whoIm()      {            return SecurityContextHolder.getContext().getAuthentication().getPrincipal();      }
View Code

我们运行,直接反问 /whoim ,则直接跳转到登录页面,我们验证过之后,再访问此url,结果如下:

到这里,我们自定义的登录已经成功了。

改造3、自定义登录成功和失败的处理逻辑

在现在的大多数应用中,一般都是前后端分离的,所以我们登录成功或失败都需要用json格式返回,或者登录成功之后,跳转到某个具体的页面。
接下来我们来实现这种改造。
 
为了实现这个功能,我们需要写2个类,分别继承 SavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandler2个类,并重写其中的部分方法即可。
//处理登录成功的。@Component("myAuthenticationSuccessHandler")public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{            @Autowired      private ObjectMapper objectMapper;      @Override      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)                  throws IOException, ServletException {                        //什么都不做的话,那就直接调用父类的方法            super.onAuthenticationSuccess(request, response, authentication);                          //这里可以根据实际情况,来确定是跳转到页面或者json格式。            //如果是返回json格式,那么我们这么写                        Map<String,String> map=new HashMap<>();            map.put("code", "200");            map.put("msg", "登录成功");            response.setContentType("application/json;charset=UTF-8");            response.getWriter().write(objectMapper.writeValueAsString(map));                                    //如果是要跳转到某个页面的,比如我们的那个whoim的则            new DefaultRedirectStrategy().sendRedirect(request, response, "/whoim");                  }}
View Code
//登录失败的@Component("myAuthenticationFailHander")public class MyAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {      @Autowired      private ObjectMapper objectMapper;      private Logger logger = LoggerFactory.getLogger(getClass());      @Override      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,                  AuthenticationException exception) throws IOException, ServletException {            // TODO Auto-generated method stub            logger.info("登录失败");            //以Json格式返回            Map<String,String> map=new HashMap<>();            map.put("code", "201");            map.put("msg", "登录失败");            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());            response.setContentType("application/json");            response.setCharacterEncoding("UTF-8");               response.getWriter().write(objectMapper.writeValueAsString(map));                  }}
View Code
代码完成之后,修改配置config类代码。
添加2个注解,自动注入
      @Autowired      private AuthenticationSuccessHandler myAuthenticationSuccessHandler;      @Autowired      private AuthenticationFailureHandler myAuthenticationFailHander;            @Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests().anyRequest().authenticated()                                    .and()                  .csrf().disable();                  }
View Code

进行测试,我们先返回json格式的(登录成功和失败的)

   

改成跳转到默认页面

改造4、添加权限控制

之前的代码我们用户的权限没有加以利用,现在我们添加权限的用法。
之前的登录验证通俗的说,就是来判断你是谁(认证),而权限控制就是用来确定:你能做什么或者不能做什么(权限)
 
在讲这个之前,我们简单说下,对于一些资源不需要权限认证的,那么就可以在Config中添加 过滤条件,如:
@Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()                        .antMatchers("/index").permitAll()  //这就表示 /index这个页面不需要权限认证,所有人都可以访问                  .anyRequest().authenticated()                               .and()                  .csrf().disable();                  }
View Code
那么我们直接访问 /index 就不会跳转到登录页面,这样我们就可以把一些不需要验证的资源以这种方式过滤,比如图片,脚本,样式文件之类的。
我们先来看第一种:在编码中写死的。
那其实权限控制也是通过这种方式来实现:
   http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()                        .antMatchers("/index").permitAll()                                      .antMatchers("/whoim").hasRole("ADMIN") //这就表示/whoim的这个资源需要有ROLE_ADMIN的这个角色才能访问。不然就会提示拒绝访问                  .anyRequest().authenticated() //必须经过认证以后才能访问                            .and()                  .csrf().disable();   
View Code

这个用户的角色哪里来,就是我们自己的UserDetailsService中返回的用户信息中的角色权限信息,这里需要注意一下就是 .hasRole("ADMIN"),那么给用户的角色时就要用:ROLE_ADMIN 

.antMatchers 这里也可以限定HttpMethod的不同要求不同的权限(用于适用于Restful风格的API).
如:Post需要 管理员权限,get 需要user权限,我们可以这么个改造,同时也可以通过通配符来是实现 如:/user/1 这种带参数的URL
. antMatchers ( "/whoim" ) . hasRole ( "ADMIN" )
       . antMatchers ( HttpMethod . POST , "/user/*" ) . hasRole ( "ADMIN" )
      . antMatchers ( HttpMethod . GET , "/user/*" ) . hasRole ( "USER" )
 
Spring Security 的校验的原理:左手配置信息,右手登录后的用户信息,中间投票器。
 从我们的配置信息中获取相关的URL和需要的权限信息,然后获得登录后的用户信息,
然后经过:AccessDecisionManager 来验证,这里面有多个投票器:AccessDecisionVoter,(默认有几种实现:比如:1票否决(只要有一个不同意,就没有权限),全票通过,才算通过;只要有1个通过,就全部通过。类似这种的。
WebExpressionVoter 是Spring Security默认提供的的web开发的投票器。(表达式的投票器
 
Spring Security 默认的是 AffirmativeBased   只要有一个通过,就通过。
有兴趣的可以 从FilterSecurityInterceptor这个过滤器入口,来查看这个流程。
内嵌的表达式有:permitAll  denyAll   等等。
每一个权限表达式都对应一个方法。
如果需要同时满足多个要求的,不能连写如 ,我们有个URL需要管理员权限也同时要限定IP的话,不能: . hasRole ( "ADMIN" ).hasIPAddress("192.168.1.1"); 
而是需要用access方法    .access("hasRole('ADMIN') and hasIpAddress('192.168.1.1')");这种。
 
那我们可以自己写权限表达式吗? 可以,稍后。。。这些都是硬编码的实现,都是在代码中写入的,这样的灵活性不够。所以我们接下来继续改造
改造4、添加基于RBAC(role-Based-access control)权限控制
这个大家可以去百度一下,一般都是由 3个部分组成,一个是用户,一个是角色 ,一个是资源(菜单,按钮),然后就是 用户和角色的关联表,角色和资源的关联表
 
核心就是判断当前的用户所拥有的URL是否和当前访问的URL是否匹配。
首先我们自己提供一个判断的接口和实现,代码如下:
/** * 返回权限验证的接口 *  * */public interface RbacService {      boolean hasPermission(HttpServletRequest request,Authentication authentication);}@Component("rbacService")public class RbacServiceImpl implements RbacService {      private AntPathMatcher antPathMatcher = new AntPathMatcher();      @Override      public boolean hasPermission(HttpServletRequest request, Authentication authentication) {            Object principal = authentication.getPrincipal();            boolean hasPermission = false;            if (principal instanceof UserDetails) { //首先判断先当前用户是否是我们UserDetails对象。                  String userName = ((UserDetails) principal).getUsername();                  Set<String> urls = new HashSet<>(); // 数据库读取 //读取用户所拥有权限的所有URL                                    urls.add("/whoim");                  // 注意这里不能用equal来判断,因为有些URL是有参数的,所以要用AntPathMatcher来比较                  for (String url : urls) {                        if (antPathMatcher.match(url, request.getRequestURI())) {                              hasPermission = true;                              break;                        }                  }            }            return hasPermission;      }}
View Code

然后在Security的配置项中添加自定义的权限表达式就可以了。

@Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .authorizeRequests()//                      .antMatchers("/index").permitAll()                    //                .antMatchers("/whoim").hasRole("ADMIN")//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必须经过认证以后才能访问                              .and()                  .csrf().disable();                  }
View Code

其中 @rbacService 就是我们自己声明的bean,在RbacServiceImpl实现类的头部注解中。

改造5、记住我的功能Remeber me

本质是通过token来读取用户信息,所以服务端需要存储下token信息
根据官方的文档,token可以通过数据库存储 数据库脚本
复制代码
CREATE TABLE persistent_logins (    username VARCHAR(64) NOT NULL,    series VARCHAR(64) NOT NULL,    token VARCHAR(64) NOT NULL,    last_used TIMESTAMP NOT NULL,    PRIMARY KEY (series));
复制代码

然后,配置好token 的存储 及数据源

 @Autowired      private DataSource dataSource;   //是在application.properites      /**       * 记住我功能的token存取器配置       * @return       */      @Bean      public PersistentTokenRepository persistentTokenRepository() {            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();            tokenRepository.setDataSource(dataSource);            return tokenRepository;      }
View Code

修改Security配置

  @Override      protected void configure(HttpSecurity http) throws Exception {            // TODO Auto-generated method stub            //super.configure(http);            http                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")                  .successHandler(myAuthenticationSuccessHandler)                  .failureHandler(myAuthenticationFailHander)                  .permitAll()  //表单登录,permitAll()表示这个不需要验证 登录页面,登录失败页面                  .and()                  .rememberMe()                        .rememberMeParameter("remember-me").userDetailsService(userDetailsService)                        .tokenRepository(persistentTokenRepository())                        .tokenValiditySeconds(60)                  .and()                  .authorizeRequests()//                      .antMatchers("/index").permitAll()                    //                .antMatchers("/whoim").hasRole("ADMIN")//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必须经过认证以后才能访问                              .and()                  .csrf().disable();      
View Code
登录之后 数据库就会有一条数据
然后,服务重新启动下,我们在看下直接访问 /whoim 的话,就可以直接访问了,不需要再登录了。
 
到此为止我们的Spring Securtiy 的基本用法已经改造完成了。
接下来,我会继续学习下Spring Security Oauth2 的内容,敬请期待。

猜你喜欢

转载自www.cnblogs.com/jpfss/p/9045771.html