WebSecurityConfigurerAdapter 中 httpBasic 和 jdbcAuthentication 的用户验证问题
Problem with user validation by httpBasic and jdbcAuthentication in WebSecurityConfigurerAdapter
我检查了来自 Whosebug 的大部分解决方案以及一些官方文章,例如:spring.io 我已经完成了基于 SpringBoot 的小型 webapi,我想更改安全级别从inMemoryAuthentication 到 jdbcAuthentication 我认为我的代码是正确的,我不知道为什么我仍然有 401 Postman.
中的响应代码
这是我的代码:
1.POM 依赖项
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 域 CLASS API 客户端
@Entity
@Table(name = "apiclient")
public class ApiClient {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
@Column(name = "username")
@Size(max = 50)
private String username;
@NotNull
@Column(name = "password")
@Size(max = 65)
private String password;
@NotNull
@Column(name = "role")
@Size(max = 15)
private String role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "ApiClient{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}
- 休息控制器
@RestController
public class HelloController {
@RequestMapping(path = "/user")
public String showUserMsg() {
return "User has logged in!!!";
}
@RequestMapping(path = "/admin")
public String showAdminMsg() {
return "Admin has logged in!!!";
}
}
- 安全CLASS
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated();
http.csrf().ignoringAntMatchers("/h2-console/**");
http.headers().frameOptions().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username='?'")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username='?'");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 自动用户注册的命令行运行程序
public class SampleDataLoader implements CommandLineRunner {
@PersistenceContext
EntityManager entityManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public void run(String... args) throws Exception {
ApiClient user = new ApiClient();
user.setRole("USER");
user.setPassword(passwordEncoder.encode("TESTPAS123"));
user.setUsername("USER");
entityManager.persist(user);
entityManager.flush();
System.out.println(user);
ApiClient admin = new ApiClient();
admin.setRole("ADMIN");
admin.setPassword(passwordEncoder.encode("TESTPAS123"));
admin.setUsername("ADMIN");
entityManager.persist(admin);
entityManager.flush();
System.out.println(admin);
}
}
启动springboot后应用h2数据库如下图:
单引号查询 username='?'
导致问题。
修改
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username='?'")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username='?'");
}
到
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username=?")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username=?");
}
查看您从 github link 分享的代码后,
在您的项目中,您没有启用 DEBUG 级别的日志。
启用 DEBUG 日志后我注意到
DEBUG - Executing prepared SQL statement [select username, password, true from apiclient where username='?']
DEBUG - Fetching JDBC Connection from DataSource
...
DEBUG - Caching SQL error codes for DataSource [com.zaxxer.hikari.HikariDataSource@3bcd426c]: database product name is 'H2'
DEBUG - Unable to translate SQLException with Error code '90008', will now try the fallback translator
...
DEBUG - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84)
您的查询无法获取 usersByUsername(因为查询中的单引号)并且 authoritiesByUsername 查询由于异常没有被触发,导致 spring-security 将用户视为 ROLE_ANONYMOUS,因此 401 (未经授权)
如果您想在 STS/Eclipse 控制台中查看日志。
1. 在 src/main/resources
下创建 logback.xml 文件
2. 复制以下代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="ALL_ROLLING_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>..\logs\SMTH_Project.%d{yyyy-MM-dd_HH}.%i.log.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy MM dd HH:mm:ss:SSS} [%-40thread] %-5level{5} - %msg %n</pattern>
</encoder>
</appender>
<appender name="ASYNC"
class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ALL_ROLLING_FILE" />
<queueSize>1000000</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level{5} - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
我检查了来自 Whosebug 的大部分解决方案以及一些官方文章,例如:spring.io 我已经完成了基于 SpringBoot 的小型 webapi,我想更改安全级别从inMemoryAuthentication 到 jdbcAuthentication 我认为我的代码是正确的,我不知道为什么我仍然有 401 Postman.
中的响应代码这是我的代码:
1.POM 依赖项
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 域 CLASS API 客户端
@Entity
@Table(name = "apiclient")
public class ApiClient {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
@Column(name = "username")
@Size(max = 50)
private String username;
@NotNull
@Column(name = "password")
@Size(max = 65)
private String password;
@NotNull
@Column(name = "role")
@Size(max = 15)
private String role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "ApiClient{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}
- 休息控制器
@RestController
public class HelloController {
@RequestMapping(path = "/user")
public String showUserMsg() {
return "User has logged in!!!";
}
@RequestMapping(path = "/admin")
public String showAdminMsg() {
return "Admin has logged in!!!";
}
}
- 安全CLASS
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated();
http.csrf().ignoringAntMatchers("/h2-console/**");
http.headers().frameOptions().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username='?'")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username='?'");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 自动用户注册的命令行运行程序
public class SampleDataLoader implements CommandLineRunner {
@PersistenceContext
EntityManager entityManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public void run(String... args) throws Exception {
ApiClient user = new ApiClient();
user.setRole("USER");
user.setPassword(passwordEncoder.encode("TESTPAS123"));
user.setUsername("USER");
entityManager.persist(user);
entityManager.flush();
System.out.println(user);
ApiClient admin = new ApiClient();
admin.setRole("ADMIN");
admin.setPassword(passwordEncoder.encode("TESTPAS123"));
admin.setUsername("ADMIN");
entityManager.persist(admin);
entityManager.flush();
System.out.println(admin);
}
}
启动springboot后应用h2数据库如下图:
单引号查询 username='?'
导致问题。
修改
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username='?'")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username='?'");
}
到
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, true"
+ " from apiclient where username=?")
.authoritiesByUsernameQuery("select username, role"
+ " from apiclient where username=?");
}
查看您从 github link 分享的代码后,
在您的项目中,您没有启用 DEBUG 级别的日志。
启用 DEBUG 日志后我注意到
DEBUG - Executing prepared SQL statement [select username, password, true from apiclient where username='?']
DEBUG - Fetching JDBC Connection from DataSource
...
DEBUG - Caching SQL error codes for DataSource [com.zaxxer.hikari.HikariDataSource@3bcd426c]: database product name is 'H2'
DEBUG - Unable to translate SQLException with Error code '90008', will now try the fallback translator
...
DEBUG - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84)
您的查询无法获取 usersByUsername(因为查询中的单引号)并且 authoritiesByUsername 查询由于异常没有被触发,导致 spring-security 将用户视为 ROLE_ANONYMOUS,因此 401 (未经授权)
如果您想在 STS/Eclipse 控制台中查看日志。
1. 在 src/main/resources
下创建 logback.xml 文件
2. 复制以下代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="ALL_ROLLING_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>..\logs\SMTH_Project.%d{yyyy-MM-dd_HH}.%i.log.zip</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy MM dd HH:mm:ss:SSS} [%-40thread] %-5level{5} - %msg %n</pattern>
</encoder>
</appender>
<appender name="ASYNC"
class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ALL_ROLLING_FILE" />
<queueSize>1000000</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level{5} - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>