一、Spring security框架简介
Spring Security
是Spring
社区的一个顶级项目,也是Spring Boot
官方推荐使用的Security
框架。除了常规的Authentication
(认证)和Authorization
(授权)之外,Spring Security
还提供了诸如ACL
,LDAP
,JAAS
,CAS
,OAuth
等高级特性以满足复杂场景下的安全需求。
spring security
的主要核心功能为 Authentication
(认证)和Authentication
(授权),所有的架构也是基于这两个核心功能去实现的:
-
Authentication
(认证):认证就是判断用户身份是否合法,例如用户名密码登录就是认证,如果一个用户拥有正确的密码,即可通过认证; -
Authorization
(授权):用户认证通过,但是每个用户的权限不同,判断用户有哪些权限以及是否有权限访问某些资源,就是授权。
而在认证和授权过程中,又会涉及到三个核心概念:
-
Principle
(User
):标识一个认证过的实体,在大多数场景下,在Spring Security
中,一个Principal
只是简单地代表一个用户,所以我当我们说一个安全实体的时候,你可以将其等同于说用户。 -
Authority
(Role
):可以理解为已认证的用户角色,如用户是老师角色或者是学生角色 -
Permission
:用户拥有的权限,比如用户权限是只读,不能添加、修改、删除操作。
这里注意Authority
和Permission
,字面上都是权限,但在Spring Security
中,Authority
是指用户的角色(role
),用户的Authority
是必须以ROLE_
开头的,比如ROLE_student
表示学生角色。
在Spring Security
中,Authority
和Permission
是两个完全独立,两者并没有必然的联系,但可以通过配置进行关联。比如,同样是学生角色,拥有不同的权限。
二、项目搭建
2.1、hello spring security
-
IDEA
创建一个Spring Boot
项目,依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
编写
Controller
:@RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello spring security"; } }
-
启动应用程序,访问地址
http://localhost:8080/hello
,出现身份验证输入框:这是因为
SpringBoot
默认的Spring Security
就是生效的,此时所有的请求接口都是被保护的,需要通过登录后才能正常访问。Spring Security
提供了一个默认的用户,用户名是user
,而密码则是启动项目的时候自动生成打印在控制台日志:Using generated security password: b0e4f3c9-1f3b-4e2e-bcba-697e148232b7
2.2、关闭默认身份验证
在启动类排除
// 排除
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class Securitystudy01Application {
public static void main(String[] args) {
SpringApplication.run(Securitystudy01Application.class, args);
}
}
2.3、自定义用户名和密码
上面的用户名是默认的,密码是启动时随机生成的。要自定义用户名和密码,只需要在配置文件application.properties
文件中添加如下配置:
spring.security.user.name = admin
spring.security.user.password = 123456
三、获取登录用户信息
@GetMapping("/get-user1")
public Authentication getUser1(){
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/get-user2")
public Authentication getUser2(Authentication authentication){
return authentication;
}
@GetMapping("/get-user3")
public Authentication getUser3(@AuthenticationPrincipal Authentication authentication){
return authentication;
}
@GetMapping("/get-user4")
public UserDetails getUser4(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
@GetMapping("/get-user5")
public UserDetails getUser5(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails;
}
四、基于内存用户认证
4.1、配置基于内存的用户
内存初始化认证信息,需重写WebSecurityConfigurerAdapter
类中的configure(AuthenticationManagerBuilder auth)
方法,通过auth
对象的inMemoryAuthentication()
方法指定认证信息:
// 添加注解
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加第一个用户
auth
// 使用内存认证
.inMemoryAuthentication()
// 设置用户名和密码
.withUser("admin").password("admin123")
// 指定角色, 空为不指定
.roles();
// 添加第二个用户
auth
// 使用内存认证
.inMemoryAuthentication()
// 设置用户名和密码
.withUser("user").password("user123")
// 指定角色, 空为不指定
.roles();
}
}
如果是5.x之前的版本,到这里启动就可以正常访问。但5.x的版本启动后,在登录页面输入账号进行访问时,报如下错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
这是因为Spring Security 5.0
中新增了多种加密方式,也改变了密码的格式,需要指定加密方式。
WebSecurityConfigurerAdapter
有三个方法:
configure(AuthenticationManagerBuilder)
:用于通过允许AuthenticationProvider
容易地添加来建立认证机制。以下定义了内置认证与内置的用户“user”和“admin”登录。AuthenticationManagerBuilder allows public void configure(AuthenticationManagerBuilder auth) { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER") .and() .withUser("admin") .password("password") .roles("ADMIN","USER"); }
configure(HttpSecurity)
:允许基于选择匹配在资源级配置基于网络的安全性。以下示例将以/admin/
开头的请求限制为具有ADMIN
角色的用户,并指定任何其他请求需要成功验证后才能访问:protected void configure(HttpSecurity http) throws Exception { http .authorizeUrls() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() }
configure(WebSecurity)
:用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。例如,以下方法将导致以/resources/
开头的任何请求被忽略认证,允许匿名访问。public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/resources/**"); }
4.2、密码加密
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置加密方式
@Bean
public PasswordEncoder passwordEncoder(){
/**
* 使用SpringSecurity推荐的BCryptpassword加密类,该加密过程是不可逆的,
* 相同的明文每一次加密,加密之后的密文都是不一样的
*
* 如果需要其他加密方式,SpringSecurity也提供有如下(在源码类PasswordEncoderFactories中可查看):
* encoders.put("ldap", new LdapShaPasswordEncoder());
* encoders.put("MD4", new Md4PasswordEncoder());
* encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
* encoders.put("noop", NoOpPasswordEncoder.getInstance());
* encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
* encoders.put("scrypt", new SCryptPasswordEncoder());
* encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
* encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
* encoders.put("sha256", new StandardPasswordEncoder());
*
* 加密方法: PasswordEncoder.encode()
* 比较方法(判断用户密码输入是否正确): PasswordEncoder.matches(输入未加密的密码,加密的密码)
*/
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
// 加密密码
.withUser("admin").password(passwordEncoder().encode("admin123"))
.roles();
auth
.inMemoryAuthentication()
// 加密密码
.withUser("user").password(passwordEncoder().encode("user123"))
.roles();
}
}
4.3、密码明文
@EnableWebSecurity
public class WebSecirutyConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
// 密码不加密,使用明文方式
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
// 配置密码不加密,下面两种方式都可以,不会加密密码
// .withUser("admin").password(passwordEncoder().encode("admin123"))
.withUser("admin").password("admin123")
.roles();
auth
.inMemoryAuthentication()
// 不会加密密码
.withUser("user").password(passwordEncoder().encode("user123"))
.roles();
}
}
五、用户角色认证
5.1、为用户配置角色
为用户设置角色:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder().encode("admin123"))
// 指定为 admin 角色
.roles("admin");
auth
.inMemoryAuthentication()
.withUser("user").password(passwordEncoder().encode("user123"))
// 指定为 user 角色
.roles("user");
}
}
5.2、开启方法安全验证
5.2.1、开启方法安全
在WebSecurityConfig
中添加注解@EnableGlobalMethodSecurity
开启方法安全验证,该注解有几个属性:
-
prePostEnabled
: 是否启用Spring Security
的pre
、post
注解,支持例如@PreAuthorize、@PostAuthorize,..
-
secureEnabled
:是否启用Spring Security
的@Secured
注解。少用,prePostEnabled
能实现相同功能 -
jsr250Enabled
:是否启用JSR-250 annotations
注解[@RolesAllowed..]
。少用,prePostEnabled
能实现相同功能
在一个应用程序中,可以启用多个类型的注解,但是只在接口、类、方法上设置一个注解。如:
开发中,最常用的就是prePostEnabled
,所以在WebSecurityConfig
进行设置为true
:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法安全验证,并设置prePostEnabled为true
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}
5.2.2、方法权限设置
用到的表达式:
对访问方法的权限设置,有两种方法:
-
在方法上配置注解:
-
@PreAuthorize:在执行方法之前进行权限校验。
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } @PreAuthorize("hasAnyRole('user')") //只能 user 角色才能访问 @GetMapping("/user") public String user() { return "user"; } @PreAuthorize("hasAnyRole('admin')") // 只能 admin 角色才能访问 @GetMapping("/admin") public String admin() { return "admin"; } @PreAuthorize("hasAnyRole('admin','admin')") // 拥有其中一个角色便可访问(也可用下面方法) @GetMapping("/admin") public String admin() { return "admin"; } @PreAuthorize("hasAnyRole('admin') or hasAnyRole('user')") // 拥有其中一个角色便可访问(和上面方法一样) @GetMapping("/userOrAdmin") public String user3() { return "userOrAdmin"; } @PreAuthorize("hasAnyRole('admin') and hasAnyRole('user')") // 同时拥有 admin 和 user 角色才能访问 @GetMapping("/userAndAdmin") public String user2() { return "userAndAdmin"; }
该注解还支持
Spring EL
表达式,可以根据参数进行校验,如:-
@PreAuthorize("#id < 10")
:方法必须有id
参数,而且参数的值必须小于10
-
@PreAuthorize("principal.username.equeals(#username)")
:方法必须有username
参数,而且参数的值必须是登录用户的用户名
@RestController public class HelloController { @PreAuthorize("#id < 10")// 只有参数id小于10才能访问 @GetMapping("/user/{id}") public String user3(@PathVariable("id") Integer id) { return "只有入参id小于10才能访问"; } @PreAuthorize("principal.username.equals.(#username)") @GetMapping("/user") public String user3(@RequestParam("username") String username) { return "参数的姓名是登录的用户名才可访问"; } }
-
-
@PostAuthorize:在执行方法之后进行校验。
@RestController public class HelloController { // 查询到用户信息后,再验证用户名是否和登录用户名一致 @PostAuthorize("returnObject.name == authentication.name") @GetMapping("/get-user") public User getUser(String name){ return userService.getUser(name); } // 验证返回的数是否是偶数 @PostAuthorize("returnObject % 2 == 0") public Integer test(){ // ... return id; } }
-
@PreFilter:对集合类型的参数执行过滤,移除结果为
false
的元素@RestController public class HelloController { @Autowired private ObjectMapper objectMapper; // 指定过滤的参数,仅偶数能进入方法,如入参[[1,2,3,4,5]],则返回[2,4] @PreFilter(filterTarget="ids", value="filterObject%2==0") @PostMapping("/test") public void test(@RequestBody List<Integer> ids){ return objectMapper.writeValueAsString(ids); } }
-
@PostFilter:
对集合类型的返回值进行过滤,移除结果为
false
的元素@RestController public class HelloController { @PostFilter("filterObject.id%2==0") public List<User> findAll(){ ... return userList; } }
-
-
处理像上面在方法上使用注解外,还可通过
Spring Security
配置类WebSecurityConfig
中设置@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { http // 对请求进行验证 .authorizeRequests() // 对所有的 /user请求 和以 /user1/开头的请求 都必须 admin角色才能访问 .antMatchers("/user", "/user1/**").hasAnyRole("ROLE_admin") // 也可指定请求方法验证 .antMatchers(HttpMethod.GET, "/userList").hasAnyRole("ROLE_admin") // 也可以通过正则表达式进行匹配 .regexMatchers("users/([^/].*?)/tokens/.*").hasAnyRole("ROLE_user") // 也可以对一些不需登录访问的请求(匿名请求),如注册用户就不需要登录后访问 .antMatchers(HttpMethod.POST,"addUser").permitAll() // 所有请求都需要登录后才能访问,这里必须放在上面所有过滤请求之后 .anyRequest().authenticated() .and() .formLogin() // 使用默认的登录页面 .and() .csrf().disable();// post请求关闭csrf验证,不然访问报错。生产环境需开启 } }
这里的配置的请求验证优先级比注解的优先级高,且这里的角色需要加
ROLE_
前缀。像上面,直接在
WebSecurityConfig
中配置请求路径的权限控制,如果请求路径过多,配置的权限控制也会过多,且权限控制与Security
配置文件混合一起,不方便维护,所以我们可以把权限控制单独提取出来:-
新建接口
PathAuthenticationProvider
:public interface PathAuthenticationProvider{ void config( ExpressionUrlAuthorizationConfigurer<HttpSecurity> .ExpressionInterceptUrlRegistry authorizeRequests); }
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry对象即是下面的
authorizeRequests()
:Ctrl
+ 鼠标左键点击进入源码便看到:
-
-
新建
PathAuthenticationProviderImpl
实现PathAuthenticationProvider
接口:@Component public class PathAuthenticationProviderImpl implements PathAuthenticationProvider { @Override public void config( ExpressionUrlAuthorizationConfigurer<HttpSecurity> .ExpressionInterceptUrlRegistry authorizeRequests) { authorizeRequests // 对所有的 /user请求 和以 /user1/开头的请求 都必须 admin角色才能访问 .antMatchers("/user", "/user1/**").hasAnyRole("ROLE_admin") // 也可指定请求方法验证 .antMatchers(HttpMethod.GET, "/userList").hasAnyRole("ROLE_admin") // 也可以通过正则表达式进行匹配 .regexMatchers("users/([^/].*?)/tokens/.*").hasAnyRole("ROLE_user") // 也可以对一些不需登录访问的请求,如注册用户就不需要登录后访问 .antMatchers(HttpMethod.POST,"addUser").permitAll() // 所有请求都需要登录后才能访问,这里必须放在上面所有过滤请求之后 .anyRequest().authenticated(); } }
这里的写法和
WebSecurityConfig
中的写法是一样的。 -
在
WebSecurityConfig
配置类中,把上面的路径权限配置引入:@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 省略... // 注入路径权限控制配置 @Autowired private PathAuthenticationProvider authProvider; @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin(); .and() .csrf().disable(); // 路径权限配置 authProvider.config(http.authorizeRequests()); } }
像这样,单独把路径权限抽取出来,方便单独对路径权限进行配置。这里只是详细说明了
http.authorizeRequests()
的路径权限抽取为单独一个类,同理,其他的配置,如http.forLogin()
等也可以抽取出来。还有一个重要配置 access()的使用,在前面的路径权限配置时,在编辑器提示可看到:
提示的配置都和这一小节开头的表达式列表一一对应:
但可看到多出了两个 not()
和access
。
对于not()
,没什么好说的,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 只要用户角色不是 ROLE_admin都可访问
.antMatchers("/user").not().hasAnyRole("ROLE_admin")
}
而对于 access
,看源码说明:
/**
* Allows specifying that URLs are secured by an arbitrary expression
*
* @param attribute the expression to secure the URLs (i.e.
* "hasRole('ROLE_USER') and hasRole('ROLE_SUPER')")
* @return the {@link ExpressionUrlAuthorizationConfigurer} for further
* customization
*/
public ExpressionInterceptUrlRegistry access(String attribute) {
if (not) {
attribute = "!" + attribute;
}
interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}
可知道,其参数输入为 security的表达式,所以access
中的参数是和方法注解@PreAuthorize()
参数是一样的,如
@Component
public class PathAuthenticationProviderImpl implements PathAuthenticationProvider {
@Override
public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests) {
authorizeRequests
// access参数为表达式
.antMatchers("/user").access("hasRole('user')")
.antMatchers("/hello").access("hasAnyRole('user','admin')")
.anyRequest().authenticated();
}
}
对于access()
,可以通过其实现基于数据库的RBAC(基于角色的权限访问控制)
权限控制,可以实现动态配置用户的角色和权限来实现授权管理,例如:
-
创建类`RbacService:
@Component public class RbacServiceImpl { public boolean hasPermission(HttpServletRequest request, Authentication authentication) { /** * 假设现在系统中的 uri的权限为: * 请求方式 uri 所需角色 * get /user ROLE_user * post /user ROLE_admin * put /user ROLE_tenant * * get /hello ROLE_user * post /hello ROLE_admin */ // 以下为模拟的数据,(实现开发中,可以通过数据库来维护 uri 的权限) Map<String, Map<String, String>> map = new HashMap<>(); Map<String, String> userMap = new HashMap<>(); userMap.put("get", "ROLE_user"); userMap.put("post", "ROLE_admin"); userMap.put("put", "ROLE_tenant"); map.put("/user", userMap); Map<String, String> helloMap = new HashMap<>(); helloMap.put("get", "ROLE_user"); helloMap.put("post", "ROLE_admin"); map.put("/hello", helloMap); Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails) { // 获取到登录用户的所拥有的角色 Collection<? extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities(); // 获取请求的 URI String requestURI = request.getRequestURI(); // 获取请求的方法 String requestMethod = request.getMethod().toLowerCase(); // 通过 URI 获取权限 Map<String, String> stringMap = map.get(requestURI); // 再通过 请求方法 获取到当前请求URI的权限角色 String uriRole = stringMap.get(requestMethod); // 判断用户是否有该角色 for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equalsIgnoreCase(uriRole)) { return true; } } } return false; } }
-
修改
PathAuthenticationProviderImpl
的权限控制:@Component public class PathAuthenticationProviderImpl implements PathAuthenticationProvider { @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests) { authorizeRequests // 所有请求都需要经过 rbacServiceImpl.hasPermission方法过滤,注意参数名称hasPermission()和类`rabcServiceImpl`一样 .anyRequest().access("@rbacService.hasPermission(request, authentication)"); } }
启动程序,所有请求都会经过
rbacServiceImpl.hasPermission(request, authentication)
方法。
六、忽略权限认证
前面配置的权限验证都是拦截认证所有的请求URI,但有些请求是无需进行拦截认证的,如静态文件js,css等,而如果使用前面的配置,则需要:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 图标访问不用认证
.antMatchers("/favicon.ico","/js/**","/css/**").permitAll()
// GET请求/register不用认证
.antMatchers(HttpMethod.GET, "/register").permitAll();
}
但其实Spring Security
提供有一个configure(WebSecurity web)
进行配置,在WebSecurityConfig
配置类重写进行配置,如:
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.mvcMatchers("/favicon.ico","/js/**","/css/**")
// 其实这里配置非静态路径有坑,不推荐,请在configure(HttpSecurity http)设置
.mvcMatchers(HttpMethod.GET, "/register");
}
七、基于数据库用户认证
7.1、引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
这里使用mybatis-plus
7.2、数据源
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
logging:
level:
com.example.bdatabaserole.mapper: debug
7.3、表及数据
对应的entity
类:
@Data
@ToString
public class UserInfo {
private Integer id;
private String username;
private String password;
private String role;
}
对应的mapper接口:
@Repository
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
插入一个用户并加密密码:
@SpringBootTest
class SpringSecurity20200328ApplicationTests {
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setUsername("user");
// 加密密码,密码匹配使用 passwordEncoder.matches(dbPwd, inputPwd)
userInfo.setPassword(passwordEncoder.encode("123"));
userInfo.setRole("user");
userInfoMapper.insert(userInfo);
}
}
7.4、配置从数据库获取用户信息验证
实现UserDetailService
,重写方法:
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Map<String, Object> map = new HashMap<>();
map.put("username", username);
UserInfo userInfo = userInfoMapper.selectByMap(map).get(0);
if (userInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 得到用户角色
String role = userInfo.getRole();
// 角色集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 角色必须以`ROLE_`开头,数据库中没有,则在这里加
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
return new User(
userInfo.getUsername(),
// 因为密码已加密,所以不用加密
userInfo.getPassword(),
authorities
);
}
}
WebSecurityConfig配置:
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
重启程序,正常使用数据库的用户登录。
八、退出登录
默认的退出登录URL为:/logout
,如:http://localhost:8080/logout
退出登录。
如果开启了CSRF,必须使用post方式请求/logout