上一篇介绍了springSecurity中常用的组件和过滤器链,明白了springSecurity管理认证和授权的基本过程和所用到的组件。之后几篇我们通过在Java代码集成springSecurity,来学习下代码上认证是如何实现的。
代码上常用的认证方式有两种,一种是实现UserDetailsService 接口,另外一种是实现AuthenticationProvider。本篇主要介绍UserDetailsService的实现。
一、代码集成SpringSecurity主要步骤
1、引入依赖
2、编写核心配置类(包含自定义过滤器,处理器等)
3、重写认证方法(两种,重写UserDetailsService 实现类或重写AuthenticationProvider实现类。(建议后者方式,获取用户信息,校验认证信息都更加灵活,之后篇章会详细介绍代码实现)
4、控制器配置注解(也可以不开启)
代码集成中,最重要的就是第2,3两步,用户自定义的过滤器,springSecurity接口实现类,策略类,处理handler等都要通过配置类配置才可以生效。
二、具体代码示例(实现UserDetailsService)
1、引入pom依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、springmvc配置类(非springSecurity必要,按需配置即可)
实现WebMvcConfigurer接口,可以重写springMvc的相关配置信息(如注册视图控制器,静态资源访问等),示例如下。注意,这个类非security必要,按需项目需要来指定添加。
代码示例:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;@Configuration // 实现springMvc的配置类,可以灵活地配置 Spring MVC 的各个方面,包括视图控制器、静态资源处理、视图解析器和 CORS 配置
public class WebConfig implements WebMvcConfigurer {// 注册视图控制器(addViewControllers),@Override
public void addViewControllers(ViewControllerRegistry registry) {// 当用户访问根路径 / 时,重定向到 /login-viewregistry.addViewController("/").setViewName("redirect:/login-view");
// 当用户访问 /login-view 时,显示 login 视图registry.addViewController("/login-view").setViewName("login");registry.addViewController("/home").setViewName("home");}// 配置静态资源的处理(addResourceHandlers)@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 当请求路径以 /static/ 开头时,从类路径下的 static 目录中查找资源文件
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}// 配置视图解析器@Override
public void configureViewResolvers(ViewResolverRegistry registry) {// 当视图名称以 .jsp 结尾时,从 /WEB-INF/views/ 目录中查找 JSP 文件registry.jsp("/WEB-INF/views/", ".jsp");}// 配置跨域资源共享(CORS)@Override
public void addCorsMappings(CorsRegistry registry) {// 允许来自 http://example.com 的请求访问所有路径,并指定的 HTTP 方法registry.addMapping("/**").allowedOrigins("http://example.com").allowedMethods("GET", "POST", "PUT", "DELETE");}
}
3、springsecurity核心配置类
最重要,包含指定权限认证,登录页,注销页,cors配置,自定义过滤器,自定义配置方法注入等。
代码示例:
import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
@EnableWebSecurity // 启用Spring Security的自动配置,并允许你通过重写方法来自定义安全配置。
@EnableGlobalMethodSecurity(securedEnabled=true) // 启用方法级别的安全配置。这个注解用于一个配置类上,允许你在方法级别上使用安全注解。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@Beanpublic BCryptPasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//指定认证对象的来源public void configure(AuthenticationManagerBuilder auth) throws Exception {// 指定认证对象方法auth.userDetailsService(userService).passwordEncoder(passwordEncoder());}//SpringSecurity配置信息public void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/login.jsp", "failer.jsp", "/css/**", "/img/**", "/plugins/**").permitAll() // 允许匿名访问的资源.antMatchers("/product").hasAnyRole("USER","ADMIN") // 有其中一个角色即可访问.antMatchers("/product1").hasRole("USER") // 有目标角色即可访问.antMatchers("/product2").hasAuthority("USER") // 有目标权限即可访问.antMatchers("/product3").hasAnyAuthority("USER","ADMIN") // 有其中一个权限即可访问.antMatchers("/product4").hasIpAddress("127.0.0.1") // 目标ip可访问.anyRequest().authenticated() // 其他请求都需要经过身份认证,认证成功即可访问(注意:如果接口上有校验权限的注解,也会根据注解校验权限).and().formLogin().loginPage("/login.jsp").loginProcessingUrl("/login").successForwardUrl("/index.jsp").failureForwardUrl("/failer.jsp").and().logout().logoutSuccessUrl("/logout").invalidateHttpSession(true).logoutSuccessUrl("/login.jsp").and().csrf().disable();}
}
4、重写UserDetailService
实现loadUserByUsername方法,返回UserDetails包含用户名,密码,权限等。
代码示例:
import com.itheima.security.springboot.dao.UserDao;
import com.itheima.security.springboot.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
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.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;@Service
public class SpringDataUserDetailsService implements UserDetailsService {@AutowiredUserDao userDao;//根据 账号查询用户信息@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//将来连接数据库根据账号查询用户信息UserDto userDto = userDao.getUserByUsername(username);if(userDto == null){//如果用户查不到,返回null,由provider来抛出异常return null;}//根据用户的id查询用户的权限List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());//将permissions转成数组String[] permissionArray = new String[permissions.size()];permissions.toArray(permissionArray);UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();return userDetails;}
}
注意一下:
这里我们需要自定义重写了UserDetailsService 实现类,里面仅包含loadUserByUsername方法,那么校验密码等到底是在哪里完成的呢?实际上认证管理是在Provider的实现类中实现的,通过源码DaoAuthenticationProvider中可以找到,这里实现校验密码。上面我们说了除了重写UserDetailsService 接口外,还可以直接重写Provider的方式,这样就可以自定义校验密码凭证了。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {if (authentication.getCredentials() == null) {
// 没有输入认证信息的直接抛异常this.logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));} else {
// 校验request的密码和uerDetails中的密码是否匹配String presentedPassword = authentication.getCredentials().toString();if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {this.logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}}
}
5、控制器
可以通过注解,设置一些接口的访问权限。如果不限制那么狠,可以缺少这一步。
常见安全注解示例:
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class LoginController {@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})public String loginSuccess(){//提示具体用户名称登录成功return getUsername()+" 登录成功";}/*** 测试资源1*/@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})@PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问public String r1(){return getUsername()+" 访问资源1";}/*** 测试资源2*/@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})@PreAuthorize("hasAuthority('p2')")//拥有p2权限才可以访问public String r2(){return getUsername()+" 访问资源2";}@Secured("ROLE_ADMIN")
public void adminOnlyMethod() {// 只有具有 ROLE_ADMIN 角色的用户才能调用此方法
}@PreAuthorize("hasRole('USER')")
public void userOnlyMethod() {// 只有具有 USER 角色的用户才能调用此方法
}@PreAuthorize("#id == authentication.principal.id")
public void getUserById(String id) {// 只有当请求的用户ID与当前认证用户ID相同时,才能调用此方法
}//获取当前用户信息private String getUsername(){String username = null;//当前认证通过的用户身份Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//用户身份Object principal = authentication.getPrincipal();if(principal == null){username = "匿名";}if(principal instanceof org.springframework.security.core.userdetails.UserDetails){UserDetails userDetails = (UserDetails) principal;username = userDetails.getUsername();}else{username = principal.toString();}return username;}
}
注意一下:
在 Spring Security 中,权限控制可以通过多种方式进行,包括配置类中的 HttpSecurity 配置和方法级别的注解(如 @PreAuthorize、@PostAuthorize、@Secured 等)。当这两种方式同时存在时,Spring Security 会如何处理权限控制呢?
处理流程:
1、请求到达:
用户发送请求到某个 URL(例如 /product1)。
2、优先HttpSecurity 配置检查:
Spring Security 会首先根据 HttpSecurity 配置进行权限检查。
如果 HttpSecurity 配置中不允许访问该 URL,请求将被拒绝,不会到达控制器方法。
3、控制器方法的注解检查:
如果 HttpSecurity 配置允许访问该 URL,请求将继续到达控制器方法。在控制器方法调用之前,Spring Security 会检查方法级别的注解(如 @PreAuthorize、@Secured 等)。如果方法级别的注解不允许访问,请求也将被拒绝,即使 HttpSecurity 配置允许访问了也不行。
总结:
如果配置类和注解都限制了接口权限,想要成功访问接口,就需要两者的条件都满足才可以,有一个不满足都无法访问。优先经过是HttpSecurity 的配置,其次校验是接口上注解。
学海无涯苦作舟!!!