title: Spring Security:限定ID的多表多身份登录验证
categories: 工程
1.前言
本文说明了如何使用Spring Security在下述情况下进行登录验证:
系统有两个身份,用户和管理员,用户和管理员的用户名和密码存储在两张MySQL表中,存在两个登录界面,保证用户只能访问用户的操作界面,管理员只能访问管理员权限;并且用户只能访问自己的主页,不能访问其他用户的主页,即页面ID(体现在网址中)与登录用户的ID一一对应.
项目使用Spring框架搭建服务器,使用MyBatis-plus操作MySQL数据库,使用Maven进行项目管理.
2.依赖
要在Maven项目中使用Spring Security框架,目前的Spring框架已经继承了Spring Security框架,因此仅需向pom.xml文件中加入Spring Security启动器依赖.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3. 登录验证
3.1开启Spring Security
开启Spring Security只需要创建配置类,该类用于配置Spring Security的规则,处理方法等
创建配置类并使其生效需要使该类继承自WebSecurityConfigurerAdapter
,并重新其中的configure(HttpSecurity http)
方法,登录验证的设置都存放在configure方法中.除此之外要为该类添加注解@EnableWebSecurity
和@Configuration
.
由于Spring Security验证密码是进行加密过的,因此需要一个密码加密器,默认使用BCryptPasswordEncoder
,创建一个方法获取密码加密器,并将其注册为Bean.密码加密器只需要注册一次,如果有多个配置类,在一个类中配置后其他类不需要再次配置.
重写configure(AuthenticationManagerBuilder auth)
用于进行登录,该方法中需要userDetailsService
对象作为登录处理的方法类和一个密码加密器用于对前端发送的登录密码进行加密.
以下是一个基础的Spring Security配置类,规则为放行所有请求:
@EnableWebSecurity
@Configuration
public class SecurityTest extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.anyRequest()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.2 配置登录验证
Spring Security的配置方法为链式编程,可以不限制地不断添加配置,不同类的配置通过.and()
隔开
开启登录验证
开启登录验证通过配置configure
函数实现,配置时以http
作为开头.
- 添加
.authorizeRequests()
表示对请求进行登录验证 - 添加
.antMatchers("/admin/**").hasRole("ADMIN")
表示对匹配"/admin/**"规则的请求需要登录用户具有"ADMIN"身份,.antMatchers("/admin/**")
可以添加多个以匹配所有请求 - 可以添加
.permitAll()
,表示未匹配到之前规则的请求全部放行,注意在该配置后的全部.antMatchers()
都会失效,因此应防止在最后 - 可以添加
.csrf().disable()
以关闭CSRF检查,开启后需要额外配置登录表单以满足检查,否则无法登录.
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.permitAll()
.csrf().disable();
}
自定登录界面
Spring Security带有默认的登录界面,但往往不能满足需求,因此需要将登录界面设置为我们的登录界面,同样在configure
中进行配置.
-
.and()
来连接不同的配置 -
.formLogin()
来说明配置登录 -
.loginPage("")
配置自定的登录界面的路由 -
.loginProcessingUrl("")
配置登录请求的路由,即要把登录表单(用户名,密码)发送到的网址. -
.successHandler()
配置登录成功处理器,即登录成功后进行的操作,用于较复杂的处理,简单的可以直接设置跳转到某个网址. -
failureHandler()
配置登录失败处理器,即登录失败后进行的操作,与登录成功处理器一致.
注意: Spring Security对登录表单具有默认值,用户名默认为"username",密码默认为"password",Spring Security默认获取提交的json表单中的这两个字段作为用户名和密码,如果提交的表单与默认值不一致,会导致无法获取到用户名和密码,可以通过修改提交的表单的键或修改Spring Security的配置,通过.passwordParameter()
和.usernameParameter()
来自定登录表单的键.
最后添加.permitALL()
使所有人都可以访问登录界面,修改后的配置如下:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.permitAll()
.and()
.formLogin()
.loginPage("/adminloginpage")
.loginProcessingUrl("/admin/login")
.successHandler(new AdminAuthenticationSuccessHandler())
.failureHandler(new AdminAuthenticationFailureHandler())
.permitAll()
.csrf().disable();
}
登出和拒绝访问
.logoutUrl("")
处理登出请求的路由,即向该路由发送请求后执行登出操作.logoutSuccessUrl("")
登出成功后跳转的界面.accessDeniedPage("/access-denied")
访问被拒绝后跳转的界面
完整的基础配置如下:
http.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/adminloginpage")
.loginProcessingUrl("/admin/login")
.successHandler(new AdminAuthenticationSuccessHandler())
.failureHandler(new AdminAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/admin/logout")
.logoutSuccessUrl("/adminloginpage")
.and()
.exceptionHandling()
.accessDeniedPage("/access-denied")
.and()
.csrf().disable();
至此即启动了基本的登录验证,而如何后端如何处理登录请求以完成登录的过程在后文说明.
4.限定ID的多表多身份登录验证
为了实现前言中的需求,需要对Spring Security的服务类进行自定义,包括登录服务,权限验证服务等,另外需要多个Spring Security配置类来区分员工和管理员
4.1管理员
对于存在多个配置类的情况,需要为每个配置了设置优先级,管理员的安全的优先级应高于普通员工,因此将管理员的优先级设为1,普通员工的优先级设为2.使用@Order()
注解为配置类设置优先级.
@Order(1)
@EnableWebSecurity
@Configuration
public class AdminSecurity extends WebSecurityConfigurerAdapter {
}
4.1.1 MyUserDetail
由于需要判断登录的用户是否可以访问/**/{userID},即userID要等于登录用户的ID,需要在Spring Security自带的UserDetail中增加一个属性,即userID,用于登录后获取登录用户的ID,自定的类即为MyUserDetail类,该类继承自Spring Security的UserDetail类.该类存储登录的相关信息,如登录用户的权限等.
由于员工和管理员都是需要一个userID属性,因此可以共用MyUserDetail类,该类如下:
public class MyUserDetail extends User {
private int userID;
public MyUserDetail(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public void setUserID(int userID) {
this.userID = userID;
}
public int getUserID(){
return userID;
}
}
4.1.2 AdminUserDetailsService
登录时需要通过数据库查询来验证用户名和密码,为登录用户分配身份(即权限),同时要为自定的MyUserDetail设置userID,因此需要一个自定的UserDetailsService类来处理登录请求.由于管理员和普通用户存储的表不同,具有的身份(权限)不同,因此要分开处理,为管理员创建AdminUserDetailsService类,继承自Spring Security的UserDetailsService类.
1) loadUserByUsername(String username)
该类中要重写父类的loadUserByUsername(String username)
的方法,该方法由Spring Security调用,传入登录的用户名,返回一个UserDetails对象,然后Spring Security验证登录的密码和UserDetail对象中存储的查询到的数据库中的密码是否一致,来判断是否能够登录.
根据需求,重写loadUserByUsername()
,使其创建一个UserDetails对象,在这里创建UserDetails的子类MyUserDetail对象,根据用户名查询管理员表,获取到对应的密码,这里需要使用与配置类中一致的密码加密器对数据库中的明文密码进行加密,因为Spring Security验证的是加密后的密码,将加密后的密码赋予MyUserDetail对象.如果没有查询到该用户名,直接返回null.
然后为MyUserDetail对象赋予身份"ROLE_ADMIN",注意这里与配置类中的hasRole("ADMIN")
不同,因为Spring Security会自动去除前缀"ROLE_“,仅对比后面的"ADMIN”.最后将查询到的管理员的ID赋予给MyUserDetail对象,然后返回.该MyUserDetail对象会绑定该登录管理员,直到该管理员登出.
2) checkUserID(Authentication authentication, int userID)
为了实现判断登录管理员是否对"/admin/{userID}"具有访问权限,就不能简单使用.hasRole("ADMIN")
进行判断,需要自己实现一个判断方法,返回boolean类型.该方法为boolean checkUserID(Authentication authentication, int userID)
传入的authentication即为登录的上下文,通过该对象获取登录信息,传入的userID即为请求网址中的{userID}.
在该方法中,首先判断用户是否登录,若未登录直接返回false,然后通过authentication获取登录用户的MyUserDetail对象,通过MyUserDetail对象获取登录用户的userID和用户的身份.如果该用户具有身份"ROLE_ADMIN"且要访问的管理员主页/admin/{userID}中的userID等于该登录用户的ID,返回true,否则返回false.
3) 完整代码
@Service
public class AdminUserDetailsService implements UserDetailsService{
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("尝试加载Admin: "+"username:"+username);
QueryWrapper<Admins> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("admin_name",username);
Admins admin=adminMapper.selectOne(queryWrapper);
if (admin == null) {
throw new UsernameNotFoundException("Admin not found with username: " + username);
}
GrantedAuthority grantedAuthority=new SimpleGrantedAuthority("ROLE_ADMIN");
BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
String password=bCryptPasswordEncoder.encode(admin.getAdminPassword());
MyUserDetail myUserDetail = new MyUserDetail(username, password, Collections.singleton(grantedAuthority));
myUserDetail.setUserID(admin.getAdminNum());
return myUserDetail;
}
public boolean checkUserID(Authentication authentication, int userID) {
//首先检验用户是否登录
if(authentication.getPrincipal().equals("anonymousUser")){
return false;
}
MyUserDetail userDetails = (MyUserDetail) authentication.getPrincipal();
//获取用户的权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 检查用户权限
if (authorities.contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
return userDetails.getUserID() == userID; // 用户拥有正确的身份和权限
}
return false;
}
}
4.1.3 AdminAuthenticationSuccessHandler
由于登录成功后需要跳转到该管理员的主页,跳转的网页是动态生成的(/admin/{userID}),因此不能简单使用.successForwardUrl()
设置一个跳转的静态网页.因此需要一个处理器来生成并跳转.
该处理器功能简单,即根据登录用户的ID动态重定向网页,它会通过Spring Security的登录上下文获取到当前登录用户的userDetail,从userDetail中获取登录用户的ID,根据该ID生成网址并跳转
@Component
public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
MyUserDetail userDetails = (MyUserDetail) authentication.getPrincipal();
// 获取用户的userID
int userID = userDetails.getUserID();
// 构建重定向URL
String targetUrl = "/admin/" + userID;
System.out.println("Redirecting to: " + targetUrl);
// 重定向到目标URL
response.sendRedirect(targetUrl);
}
}
4.1.4 AdminAuthenticationFailureHandler
与AdminAuthenticationSuccessHandler相似,处理登录失败跳转到的界面,即登录界面,但网址中添加了error=true
参数,因为登录网页使用了thymelefa模板,根据添加的error参数来动态显示登录出错的提示信息
@Component
public class AdminAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 设置自定义错误消息
setDefaultFailureUrl("/adminloginpage?error=true"); // 这里设置了一个错误参数来标识登录失败
super.onAuthenticationFailure(request, response, exception);
}
}
4.1.5 AdminSecurity
最终根据自定的各种组件来配置Spring Security的配置类,实现对于管理员的登录检查.
注意: 对于多Spring Security配置类的情况,要保证未被优先级高的配置类匹配的网址继续被低优先级的配置类尝试匹配,需要为每一个配置类设置网站过滤条件,否则所有网址都会被第一个配置类捕获,如果没有匹配成功,则会被直接放行而不会被下一个配置类处理.
在设置.authorizeRequests()
之前,设置.antMatcher("")
,表明该配置类只处理符合antMatcher中规则的网址,不符合的网址会被后续配置类尝试处理.
最终的AdminSecurity配置类代码如下:
@Order(1)
@EnableWebSecurity
@Configuration
public class AdminSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private AdminUserDetailsService adminUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/workers-control").hasRole("ADMIN")
.antMatchers("/admin/departments-control").hasRole("ADMIN")
.antMatchers("/admin/worker/**").hasRole("ADMIN")
.antMatchers("/admin/register").hasRole("ADMIN")
.antMatchers("/admin/department/**").hasRole("ADMIN")
.antMatchers("/admin/leave/**").hasRole("ADMIN")
.antMatchers("/admin/evection/**").hasRole("ADMIN")
.antMatchers("/admin/query/**").hasRole("ADMIN")
.antMatchers("/admin/analyze/all").hasRole("ADMIN") .antMatchers("/admin/{userID}").access("@adminUserDetailsService.checkUserID(authentication, #userID)")
.and()
.formLogin()
.loginPage("/adminloginpage")
.loginProcessingUrl("/admin/login")
.successHandler(new AdminAuthenticationSuccessHandler())
.failureHandler(new AdminAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/admin/logout")
.logoutSuccessUrl("/adminloginpage")
.and()
.exceptionHandling()
.accessDeniedPage("/access-denied")
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminUserDetailsService).passwordEncoder(passwordEncoder2());
}
@Bean
public PasswordEncoder passwordEncoder2() {
return new BCryptPasswordEncoder();
}
}
4.2 普通用户
普通用户的Spring Security登录验证的配置与组件与管理员基本一致,因此不再详细说明,仅对不同的部分进行说明.
4.2.1 MyUserDetail
普通用户与管理员共用一个MyUserDeatil
4.2.2 MyUserDetailsService
各函数的功能和流程均与AdminUserDetilService一致
1) loadUserByUsername(String username)
根据用户名查询密码和ID的数据库表由管理员表Admin改为普通用户表workers
2) checkUserID(Authentication authentication, int userID)
判断登录用户的身份时,身份由管理员(ROLE_ADMIN)改为普通用户(ROLE_USER)
3) 完整代码
@Service
public class MyUserDetailsService implements UserDetailsService{
@Autowired
private WorkersMapper workersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("尝试加载User: "+"username:"+username);
QueryWrapper<Workers> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("worker_name",username);
Workers worker=workersMapper.selectOne(queryWrapper);
if (worker == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
GrantedAuthority grantedAuthority=new SimpleGrantedAuthority("ROLE_USER");
BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
String password=bCryptPasswordEncoder.encode(worker.getPassword());
MyUserDetail myUserDetail = new MyUserDetail(username, password, Collections.singleton(grantedAuthority));
myUserDetail.setUserID(worker.getWorkerNum());
return myUserDetail;
}
public boolean checkUserID(Authentication authentication, int userID) {
//首先检验用户是否登录
if(authentication.getPrincipal().equals("anonymousUser")){
return false;
}
MyUserDetail userDetails = (MyUserDetail) authentication.getPrincipal();
//获取用户的权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 检查用户权限
if (authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))) {
return userDetails.getUserID() == userID; // 用户拥有正确的身份和权限
}
return false;
}
}
4.2.3 CustomAuthenticationSuccessHandler
跳转的主页由*/admin/{userID}改为/user/{userID}*
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
MyUserDetail userDetails = (MyUserDetail) authentication.getPrincipal();
// 获取用户的userID
int userID = userDetails.getUserID();
// 构建重定向URL
String targetUrl = "/user/" + userID;
System.out.println("用户登录成功,跳转到"+targetUrl);
// 重定向到目标URL
response.sendRedirect(targetUrl);
}
}
4.2.4 CustomAuthenticationFailureHandler
跳转的页面由管理员登录界面*/adminloginpage?error=true改为普通用户登录界面/loginpage?error=true*
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 在这里可以自定义处理登录失败的逻辑,例如记录日志或者返回自定义的错误消息
// 设置自定义错误消息
setDefaultFailureUrl("/loginpage?error=true"); // 这里设置了一个错误参数来标识登录失败
super.onAuthenticationFailure(request, response, exception);
}
}
4.2.5 CustomSecurity
设置普通用户Spring Security配置类的优先级为2(低于管理员优先级即可,数字越大优先级越低),即Order(2)
将网址过滤条件从*/admin/改为/user/*,将处理组件改为普通用户的处理组件,修改登录页面网址等.
@Order(2)
@EnableWebSecurity
@Configuration
public class CustomSecurity extends WebSecurityConfigurerAdapter {
@Autowired-
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/user/**")
.authorizeRequests() .antMatchers("/user/{userID}").access("@myUserDetailsService.checkUserID(authentication, #userID)") .antMatchers("/user/info/{userID}").access("@myUserDetailsService.checkUserID(authentication, #userID)")
.and()
.formLogin()
.loginPage("/loginpage")
.loginProcessingUrl("/user/login")
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/loginpage")
.and()
.exceptionHandling()
.accessDeniedPage("/access-denied")
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5. 总结
Spring Security是一个基于Spring框架的安全性和身份验证框架,旨在为Java应用程序提供全面的安全解决方案。它为应用程序提供了一套强大的安全性服务,包括身份验证、授权、会话管理等功能.
Spring Security无缝集成到Spring框架中,为开发人员提供了一致性的编程和配置模型。这种集成性使得将安全性添加到现有的Spring应用程序变得相对简单,同时确保了代码的清晰性和可维护性。并且Spring Security支持多种身份验证机制,包括基本认证、表单登录、OAuth等,使得开发人员能够选择适合其应用程序需求的认证方式。同时,它提供了细粒度的授权机制,允许开发人员定义对应用程序资源的精确访问规则.
Spring Security具有高度的自定义性和高拓展性,可以通过自定义组件,与其他框架如MyBatis兼容,具有极高的灵活性:自定义认证提供者,开发人员可以实现自己的认证逻辑以满足特殊的身份验证需求.这使得可以集成多种认证机制,并根据应用程序的要求选择合适的认证方式;自定义用户详细信息服务,连接不同的用户存储后端,如数据库、LDAP等;自定义访问决策管理器,自定义对应用程序资源的访问规则,实现更细粒度的授权策略,满足特定的业务逻辑;可以使用表达式语言进行授权,这使得可以在配置文件中定义更加灵活和动态的授权规则,通过表达式,可以基于方法调用、请求路径等条件灵活地进行授权判断.本文实现的功能便是基于Spring Security的高灵活性和拓展性实现的.