十、Spring security 配置基于jpa的认证登录
项目环境:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
加入jpa 。注意,如果引入了jpa,则不需要再引入jdbc,因为jpa默认引用了jdbc
<!--加入数据库jpa连接-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
数据库使用H2,仅作测试使用
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
配置h2数据库和jpa属性
# H2数据源
datasource:
driver-class-name: org.h2.Driver
# 数据库连接url
# MODE=MySQL:兼容MySQL写法
# DATABASE_TO_LOWER=TRUE :表名转小写
# CASE_INSENSITIVE_IDENTIFIERS=TRUE : 不区分大小写
# DB_CLOSE_DELAY=-1 : 不自动关闭数据库连接
url: jdbc:h2:mem:mydb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1
username: sa
password:
jpa:
hibernate:
# 官方说明: https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization.using-jpa
ddl-auto: create-drop
database-platform: org.hibernate.dialect.H2Dialect
database: h2
# 官方说明:https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization
# 这将推迟数据源初始化,直到创建和初始化任何 EntityManagerFactory beans 之后
# 解决了data.sql文件执行报错的问题,原理就是创建好表实体产生表结构后,再执行初始化
defer-datasource-initialization: true
h2:
console:
# 显示H2嵌入式UI管理界面
enabled: true
# 访问H2 UI界面路径
path: /h2-console
settings:
trace: false
web-allow-others: false
实体和mapper定义
User.java
import lombok.Data;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.GeneratorType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
@Data
@Entity
@Table(name = "mooc_users")
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 固定参数
/**
* 用户名
*/
@Column(length = 50, unique = true, nullable = false)
private String username;
/**
* 密码
*/
@Column(name = "password_hash", length = 255, nullable = false)
private String password;
/**
* 指示用户的帐户是否已过期。过期的帐户无法进行身份验证。
* 如果用户的帐户有效(即未过期)则为true ,如果不再有效(即已过期) false
*/
@Column(name = "account_non_expired", nullable = false)
private boolean accountNonExpired;
/**
* 指示用户是锁定还是解锁。无法对锁定的用户进行身份验证。
* 如果用户未被锁定, true ,否则为false
*/
@Column(name = "account_non_locked", nullable = false)
private boolean accountNonLocked;
/**
* 指示用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证。
* 如果用户的凭据有效(即未过期), true ;如果不再有效(即,已过期), false
*/
@Column(name = "credentials_non_expired", nullable = false)
private boolean credentialsNonExpired;
/**
* 指示用户是启用还是禁用。无法对禁用的用户进行身份验证。
* 如果用户已启用, true ,否则为false
*/
@Column(nullable = false)
private boolean enabled;
@ManyToMany
@Fetch(FetchMode.JOIN)
@JoinTable(name = "mooc_users_roles",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}
)
private Set<Role> authorities;
// 额外参数
@Column(length = 255, unique = true)
private String email;
@Column(length = 50)
private String name;
@Column(length = 11)
private String mobile;
}
Role.java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import javax.persistence.*;
import java.io.Serializable;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "mooc_roles")
public class Role implements GrantedAuthority, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 固定参数
@Column(name = "role_name", unique = true, nullable = false, length = 50)
private String authority;
}
UserRepo.java
import com.example.uaa.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepo extends JpaRepository<User, Long> {
User findByUsername(String username);
}
RoleRepo.java
import com.example.uaa.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepo extends JpaRepository<Role, Long> {
}
自定义UserDetailServiceImpl.java
import com.example.uaa.dao.UserRepo;
import com.example.uaa.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
// @Service
@RequiredArgsConstructor
public class UserDetailServiceImpl implements UserDetailsService {
private final UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username);
if(user==null) {
throw new UsernameNotFoundException("未找到用户名为'" +username+ "'的用户");
}
return user;
}
}
SecurityConfig.java
import com.example.uaa.dao.UserRepo;
import com.example.uaa.filter.RestAuthenticationFilter;
import com.example.uaa.service.impl.UserDetailServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.passay.MessageResolver;
import org.passay.spring.SpringMessageResolver;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.sql.DataSource;
import java.util.Map;
/**
* core配置
*/
@Slf4j
@Configuration
@EnableWebSecurity(debug = false) // 开启调试模式,生产环境不要使用
@RequiredArgsConstructor
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final AuthenticationConfiguration authenticationConfiguration;
private final MessageSource messageSource;
// private final DataSource dataSource;
private final UserRepo userRepo;
// @Bean
// public DataSource dataSource() {
// return new EmbeddedDatabaseBuilder()
// .setType(EmbeddedDatabaseType.H2)
// .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)// 自动建表
// .build();
// }
/**
* 注册passay的国际化消息处理类
*
* @return
*/
@Bean
public MessageResolver messageResolver() {
return new SpringMessageResolver(messageSource);
}
/**
* 自定义验证注解,添加国际化消息处理类
* @return
*/
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((req) -> req
// 需要认证并拥有特定角色才能访问的URL
.antMatchers("/api/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
// 放行不需要认证的URL
.antMatchers("/authorize/**").permitAll()
.antMatchers("/webjars/**", "/error", "/h2-console/**").permitAll()
// 放行swagger3
.antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/**", "/doc.html").permitAll()
// 放行通用的css、js等静态资源位置
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// 除了以上规则,默认拦截所有URL请求,要求认证后才能访问(该语句必须要放在末尾)
.anyRequest().authenticated()
)
// 用于配置H2控制台界面的正常显示 https://docs.spring.io/spring-boot/docs/2.7.11/reference/html/data.html#data.sql.h2-web-console.spring-security
.headers((headers) -> headers.frameOptions().sameOrigin())
// 添加自定义登录过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 以下表单登录,被restAuthenticationFilter配置替换:
// .formLogin(form-> form
// // 定义登陆页面
// .loginPage("/login").permitAll()
// // 登录成功后跳转的页面
// // .defaultSuccessUrl("/")
// // 登录成功后置处理器。会覆盖掉defaultSuccessUrl的配置
// .successHandler(((request, response, authentication) -> {
// ObjectMapper objectMapper = new ObjectMapper();
// response.setStatus(HttpStatus.OK.value());
// response.getWriter().println("LoginSuccess:"+
// objectMapper.writeValueAsString(authentication));
// }))
// // 登录失败后置处理器,不配置,默认跳转/login?error
// .failureHandler(((request, response, exception) -> {
// response.setStatus(HttpStatus.UNAUTHORIZED.value());
// response.getWriter().println("LoginFailure!");
// }))
// )
// 配置退出登录的路径,配置后使用POST请求。若不配置,默认为/logout,退出成功跳到/login
// .logout(logout-> logout.logoutUrl("/logout").logoutSuccessUrl("/login"))
.httpBasic(Customizer.withDefaults())
// 开启CSRF ,并配置不需要CSRF的路径
// .csrf(csrf->csrf.ignoringAntMatchers("/api/**"))
// .csrf()
// 禁用csrf
.csrf().disable()
.rememberMe(Customizer.withDefaults());
// ...
return http.build();
}
/**
* 设定认证方式 Authentication
* 代替了配置
* spring.security.user.name=user
* spring.security.user.password=12345
* spring.security.user.roles=ADMIN,USER
*
* 基于spring boot 2.7 后新的认证方式:
* https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
*/
@Bean
public UserDetailsService userDetails() {
return new UserDetailServiceImpl(userRepo);
}
@Bean
static PasswordEncoder passwordEncoder() {
// 数据库存储格式,{bcrypt}xxxx ,例如 '{bcrypt}$2a$10$hpROwWyEu4AbMJpFF2jLBusGdRnOK2VktGy3VVnrsmkC98TFWnJ.K'
val idForEncode = "bcrypt";
return new DelegatingPasswordEncoder(idForEncode,Map.of(idForEncode, new BCryptPasswordEncoder()));
// return new BCryptPasswordEncoder();
}
/**
* 声明
* @return
* @throws Exception
*/
private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
// 登录成功后置处理器。
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.getWriter().println("LoginSuccess: "+
objectMapper.writeValueAsString(authentication));
});
// 登录失败后置处理器
filter.setAuthenticationFailureHandler((request, response, exception) -> {
log.error("认证失败", exception);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("LoginFailure!");
});
// 获得AuthenticationManager
filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
// 设置登登录请求地址
filter.setFilterProcessesUrl("/authorize/login");
return filter;
}
}
说明:
UserDetailsService: 通过自定义的UserDetailServiceImpl.java,实现了获取数据库用户身份。
RestAuthenticationFilter: 自定义登录过滤器,并声明了请求登录的地址为:/authorize/login
自定义登录过滤器,使用json格式传参数。返回json数据\
RestAuthenticationFilter.java
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登录过滤器,使用json格式传参数。返回json数据
*/
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持的请求方法: " + request.getMethod());
}
UsernamePasswordAuthenticationToken authRequest;
try {
var jsonNode = objectMapper.readTree(request.getInputStream());
String username = jsonNode.get("username").textValue();
String password = jsonNode.get("password").textValue();
authRequest = new UsernamePasswordAuthenticationToken(username, password);
} catch (IOException e) {
e.printStackTrace();
throw new BadCredentialsException("参数解析出错!");
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// AuthenticationManager 与 UserDetailService之间的关,以及如何使用UserDetailService来完成认证请看:
// https://docs.spring.io/spring-security/reference/5.7/servlet/authentication/passwords/dao-authentication-provider.html
return this.getAuthenticationManager().authenticate(authRequest);
}
}
添加初始化data.sql
将下面的sql放到/src/main/resource/data.sql,项目启动的时候将会自动执行。
insert into mooc_users (id, username, `name`, mobile, password_hash, enabled, account_non_expired, account_non_locked, credentials_non_expired, email)
values (1, 'user', 'Zhang San', '13012341234', '{bcrypt}$2a$10$hpROwWyEu4AbMJpFF2jLBusGdRnOK2VktGy3VVnrsmkC98TFWnJ.K', 1, 1, 1, 1, 'zhangsan@local.dev'),
(2, 'old_user', 'Li Si', '13812341234', '{SHA-1}7ce0359f12857f2a90c7de465f40a95f01cb5da9', 1, 1, 1, 1, 'lisi@local.dev');
insert into mooc_roles (id, role_name) values (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
insert into mooc_users_roles (user_id, role_id) values (1, 1), (1, 2), (2, 1);
身份认证流程图,逻辑解释

-
The authentication Filter from Reading the Username & Password passes a UsernamePasswordAuthenticationToken to the AuthenticationManager which is implemented by ProviderManager.
-
The ProviderManager is configured to use an AuthenticationProvider of type DaoAuthenticationProvider.
-
DaoAuthenticationProvider looks up the UserDetails from the UserDetailsService.
-
DaoAuthenticationProvider then uses the PasswordEncoder to validate the password on the UserDetails returned in the previous step.
-
When authentication is successful, the Authentication that is returned is of type UsernamePasswordAuthenticationToken and has a principal that is the UserDetails returned by the configured UserDetailsService. Ultimately, the returned UsernamePasswordAuthenticationToken will be set on the SecurityContextHolder by the authentication Filter.

浙公网安备 33010602011771号