目录
学习目标
一.SpringSecurity
1.SpringSecurity简介及快速入门
<1>.SpringSecurity简介
Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有很多,但大部分都是发现了javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证”和“授 权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情
<2>.SpringSecurity快速入门
导入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>springsecuritydemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springsecuritydemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--SpringSecurity组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql 数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!--thymeleaf springsecurity5 依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
导入前端页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username123" /><br/>
密码:<input type="password" name="password123" /><br/>
记住我:<input type="checkbox" name="remember-me" value="true" /><br />
<input type="submit" value="登录" />
</form>
</body>
</html>
访问页面:
导入spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
默认的username为user,密码打印在控制台上,在浏览器中输入账号和密码后会显示 login.html 页面内容。
2.SpringSecurity基本原理
<1>.UserDetailsService详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService 接口即可。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
返回值是一个UserDetails接口:
public interface UserDetails extends Serializable {
// ~ Methods
Collection<? extends GrantedAuthority> getAuthorities();//获取所有权限
String getPassword();//获取密码
String getUsername();//获取用户名
boolean isAccountNonExpired();//判断账号是否过期
boolean isAccountNonLocked();//判断账号是否被锁定
boolean isCredentialsNonExpired();//判断凭证(密码)是否过期
boolean isEnabled();//是否可用
}
这里我们一般返回UserDetails的实现类User
<2>.PasswordEncoder 密码解析器详解
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder 的bean对象。
接口介绍:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);//把参数按照特定的解析规则进行解析。
//:验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;
如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
//如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器
代码示例:
@Test
public void contextLoads() {
PasswordEncoder pe = new BCryptPasswordEncoder();
String encode = pe.encode("123");
System.out.println(encode);
boolean matches = pe.matches("1234", encode);
System.out.println("========================");
System.out.println(matches);
}
3. SpringSecurity自定义登录逻辑及权限控制
<1>.自定义逻辑登录
当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的 UserDetailsService 和 PasswordEncoder 。但是Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder 实例。所以不能直接 new 对象。
代码示例:
第一步:编写配置类,注入PasswordEncoder Bean:
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
第二步:编写自定义登录逻辑(账号密码需要从数据库获取,这里使用模拟的方法)
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("执行了loadUserByUsername方法");
//1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在!");
}
//2.把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
String password = pw.encode("123");
//加入相应的权限
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
"/main.html,/insert,/delete"));
}
}
重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。
<2>.自定义登录页面
1.修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure方法。
successForwardUrl() //登录成功后跳转地址
loginPage() //登录页面
loginProcessingUrl //登录页面表单提交地址,此地址可以不真实存在。
antMatchers() //匹配内容
permitAll() //允许
<3>.自定义成功处理器
使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler() 方法进行控制成功后交给哪个类进行处理
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println(request.getRemoteAddr());
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
//输出null(保密)
System.out.println(user.getPassword());
System.out.println(user.getAuthorities());
response.sendRedirect(url);
}
}
<4>.自定义失败处理器
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
<5>.自定义登录配置代码示例
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
.usernameParameter("username123")
.passwordParameter("password123")
//当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
//自定义登录页面
.loginPage("/showLogin")
//登录成功后跳转页面,Post请求
.successForwardUrl("/toMain")
//登录成功后处理器,不能和successForwardUrl共存
// .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
//登录失败后跳转页面,Post请求
.failureForwardUrl("/toError");
//登录失败后处理器,不能和failureForwardUrl共存
// .failureHandler(new MyAuthenticationFailureHandler("/error.html"));
}
4.访问控制及角色权限判断
<1>.访问控制URL匹配
在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面
(1).anyRequest()
在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。
.anyRequest().authenticated();
(2).antMatcher()
public C antMatchers(String... antPatterns)
参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
规则如下:
? : 匹配一个字符
*:匹配 0 个或多个字符
** :匹配 0 个或多个目录
(3).regexMatchers()
使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数, antMatchers() 参数是 ant 表达式,regexMatchers() 参数是正则表达式
<2>.内置访问控制方法
1.permitAll()
permitAll()表示所匹配的 URL 任何人都允许访问
2.authenticated()
authenticated()表示所匹配的 URL 都需要被认证才能访问。
3.anonymous()
anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter链中
4.denyAll()
denyAll()表示所匹配的5. URL 都不允许被访问。
rememberMe()
被“remember me”的用户允许访问
5.fullyAuthenticated()
如果用户不是被 remember me 的,才可以访问
<3>.角色权限判断
1.hasAuthority(String)
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。 admin和normal 就是用户的权限。admin和normal 严格区分大小写
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
"/main.html,/insert,/delete"));
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
.antMatchers("/main1.html").hasAuthority("admin")
2.hasAnyAuthority(String …)
如果用户具备给定权限中某一个,就允许访问。下面代码中由于大小写和用户的权限不相同,所以用户无权访问
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
3.hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。
参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
"/main.html,/insert,/delete"));
.antMatchers("/main1.html").hasRole("abc")
4.hasAnyRole(String …)
如果用户具备给定角色的任意一个,就允许被访问
5.hasIpAddress(String)
如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr() 获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
当浏览器中通过 localhost 进行访问时控制台打印的内容:0:0:0:0:0:0:0:1
角色权限判断代码示例:
//授权认证
http.authorizeRequests()
//error.html不需要被认证
// .antMatchers("/error.html").permitAll()
.antMatchers("/error.html").access("permitAll()")
//login.html不需要被认证
// .antMatchers("/login.html").permitAll()
.antMatchers("/showLogin").access("permitAll()")
.antMatchers("/js/**","/css/**","/images/**").permitAll()
// .antMatchers("/**/*.png").permitAll()
//正则表达式匹配
// .regexMatchers(".+[.]png").permitAll()
// .regexMatchers(HttpMethod.GET,"/demo").permitAll()
//mvc匹配servletPath为特有方法,其他2种匹配方式没有
// .mvcMatchers("/demo").servletPath("/xxxx").permitAll()
//和mvc匹配等效
// .antMatchers("/xxxx/demo").permitAll()
//权限判断
// .antMatchers("/main1.html").hasAuthority("admiN")
// .antMatchers("/main1.html").hasAnyAuthority("admin","admiN")
//角色判断
// .antMatchers("/main1.html").hasRole("abC")
// .antMatchers("/main1.html").access("hasRole('abc')")
// .antMatchers("/main1.html").hasAnyRole("abC,abc")
//IP地址判断
// .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
//所有请求都必须被认证,必须登录之后被访问
.anyRequest().authenticated();
//access自定义方法
// .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
<4>.自定义403处理方案
当没有权限时,浏览器会显示403状态码,这里自定义处理403,让他返回一个显示无权限的页面
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置响应状态码
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
writer.flush();
writer.close();
}
}
配置类中添加异常处理器。设置访问受限后交给哪个对象进行处理,myAccessDeniedHandler 是在配置类中进行自动注入的。
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
<5>.基于注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity 进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500
1.@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
```java
/**
* 成功后跳转页面
* @return
*/
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
2.@PreAuthorize/@PostAuthorize
@PreAuthorize 和@PostAuthorize 都是方法或类级别注解。
@PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。
<6>.RememberMe功能实现
默认为两周
在SecurityConfig中添加RememberMeConfig和UserDetailsService实现类对象,并自动注入。
在 configure 中添加下面配置内容。
//记住我
http.rememberMe()
//失效时间,单位秒
.tokenValiditySeconds(60)
// .rememberMeParameter()
//自定义登录逻辑
.userDetailsService(userDetailsService)
//持久层对象
.tokenRepository(persistentTokenRepository);
<7>.退出
为了实现更好的效果,通常添加退出的配置。默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout(可用修改)
//退出登录
http.logout()
.logoutUrl("/logout")
//退出登录跳转页面
.logoutSuccessUrl("/login.html");
5.CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
Spring Security中的CSRF:从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
二. Oauth2认证
1.Oauth2
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务
如微信认证:
2.授权模式
<1>授权码模式
需要传入 code、client_id 以及 client_secret。验证通过后,返回 access_token 和 refresh_token。一旦换取成功,code 立即作废,不能再使用第二次。流程图如下:
<2>密码模式
密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向 “服务商提供商” 索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码
<3>客户端模式
鉴权服务器直接对客户端进行身份验证,验证通过后,返回 token。
<4>刷新令牌
如图所示:
三.Spring Security Oauth2
1.授权服务器
Authorize Endpoint :授权端点,进行授权
Token Endpoint :令牌端点,经过授权拿到对应的Token
Introspection Endpoint :校验端点,校验Token的合法性
Revocation Endpoint :撤销端点,撤销授权
2.Spring Security Oauth2架构
- 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获并重定向到认证服务器
- 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端
- 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端
- 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。
四.JWT
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
1.JWT的组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
<1>头部
头部用于描述关于该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象。
{
“alg”: “HS256”,
“typ”: “JWT”
}
typ :是类型。
alg :签名的算法,这里使用的算法是HS256算法
<2>负载
第二部分是负载,就是存放有效信息的地方。分为三部分
标准中注册的声明:
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
<3>签名
- header (base64后的)
- payload (base64后的)
- secret(盐,一定要保密)
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
五.4种常见的认证机制
1.HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,风险大
2.Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。
3.OAuth
OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
4.Token Auth
<1>Token Auth的流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
<2>获取token的主要流程
1.用户发起获取token的请求。
2.过滤器会验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作。
3.过滤器通过clientId查询生成一个Authentication对象。
4.然后会通过username和生成的Authentication对象生成一个UserDetails对象,并检查用户是否存在。
5.以上全部通过会进入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
6.postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
7.之后调用AbstractTokenGranter中的grant方法。
8.grant方法中调用AbstractUserDetailsAuthenticationProvider的authenticate方法,通过username和Authentication对象来检索用户是否存在。
9.然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
10.然后将OAuth2AccessToken对象包装进响应流返回。