SpringSecuriry
SpringSecuriry简介:基于Spring提供声明式的安全访问可控制解决方案的安全框架(认证+授权),充分利用了IOC、DI、AOP功能。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
自定义登录逻辑
SpringSecurity规定使用时容器必须有PasswordEncode的实例,故需要用配置类注入
PasswordEncoder接口:实现对密码进行加密和加密后的校验,这边推荐使用他的BCryptPasswordEncoder实现类。
在Spring Security中,用户输入的密码和查询到的密码(通常为哈希值)的一致性判断是通过PasswordEncoder
实现的,具体过程如下:
1. 用户认证请求:当用户尝试登录时,其提供的用户名和未加密的明文密码会被封装进一个UsernamePasswordAuthenticationToken
对象中。
2. 认证管理器调用:接下来,这个AuthenticationToken
会被传递给AuthenticationManager
,这是Spring Security中处理认证请求的核心组件。
3. UserDetailsService调用:AuthenticationManager
根据UsernamePasswordAuthenticationToken
中的用户名找到对应的UserDetailsService
实现类,并调用loadUserByUsername
方法来加载用户信息。这个方法返回一个包含用户信息的UserDetails
对象,其中就包含了用户加密后的密码(哈希值)。
4. 密码校验:拥有用户信息后,AuthenticationManager
会利用配置好的PasswordEncoder
来校验用户输入的明文密码。它通过以下步骤完成:
@Configuration
public class SecurityConfig{
@Bean
public PasswordEncoder getPasswordEncode(){
return new BCryptPasswordEncoder();
}
}
自定义登录校验逻辑
@Service
public class UserSecurityServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pe;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.查询数据库,获取用户名和密码
User u=userMapper.findUser(username);
//2。拿密码(密文)进行解析
String password=pe.encode(u.getPassword);
//返回的User和上面的User不是一个类,此处返回的User为SpringSecurity中的,AuthorityUtils.commaSeparatedStringToAuthorityList用于生成权限
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_A"));
//admin和normal为权限,而ROLE_XXX为角色
}
}
自定义登录页面
1.配置类补全
在 ‘自定义登录逻辑’ 配置的Config中继承WebSecurityConfigurerAdapter类并实现方法实现。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/*
WebSecurityConfigurerAdapter 是 Spring Security 早期版本中用于自定义安全配置的核心类。从 Spring Security 5.7 版本开始,此类已经被标记为过时,并在 Spring Security 6 中彻底移除。
在新版 Spring Security 中,你可以直接实现 WebSecurityConfigurer 接口或使用 lambda 表达式来自定义安全配置:
*/
@Autowired
private UserSecurityServiceImpl userSecurityService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder getPasswordEncode(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//表单提交
//设置表单中登录的别名,默认为username和password
.usernameParameter("uName")
.passwordParameter("pWord")
//当发现/submit认为是登录逻辑,此处必须与表单提交的地址一致,才会去执行UserSecurityServiceImpl
.loginProcessingUrl("/submit")
.loginPage("login.html")//自定义登录页
//成功跳转必须是Post请求,所以需要在Controller中自定义跳转方法
.successForwardUrl("/toMain")
//这个是错误的跳转页面,Controller必须是Post
.failureForwardUrl("/toError");
http.authorizeRequests()
//login.html为登录页,无需认证
.antMatchers("/login.html").permitAll()
//错误页面和登录页面都不需要被认证,下面为正则表达式,功能与上方一致
// .regexMatchers("/error.html").permitAll() 此方法效果一致
.antMatchers("/error.html").permitAll()
.anyRequest().authenticated();
//csrf防护
http.csrf().disable();
}
//将自定义的userDetail登录逻辑注册进配置类
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder);
}
}
自定义登录处理器
面对前后端分离项目,如果使用上面的方法(successForwardUrl和failForwardUrl)无法跳转到在外的页面,需要定义一个处理器
1.自定义登录成功和失败处理器
public class PasswordSuccessHandle implements AuthenticationSuccessHandler {
private String url;
public PasswordSuccessHandle(String url){
this.url=url;
}
//重定向至相应的页面
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//可添加其他逻辑
response.sendRedirect(url);
}
}
public class PasswordFailHandle implements AuthenticationFailureHandler {
private String url;
public PasswordFailHandle(String url){
this.url=url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//可添加其他逻辑
response.sendRedirect(url);
}
}
2.覆盖config中的相应配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//........
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//表单提交
//............
.loginPage("login.html")//自定义登录页
//仅仅覆盖此处的配置,使用了自定义的处理器,参数为需要跳转的外部url
.successHandler(new PasswordSuccessHandle("http://baidu.com"))
.failureHandler(new PasswordFailHandle("/fail.html"));
//......
}
}
权限判断\IP判断
在上文登录校验中在登录成功后赋予了自定义的若干角色和权限,而下面对权限的配置是对其使用的方法之一
目前在MVC中,request.getRemoteAddr()获取访问的ip地址,同理Security也可以对ip地址进行权限的筛选。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
http.authorizeRequests()
//......
//下面对角色权限的控制,只有指定的角色才能访问,角色取ROLE_XXX后面的XXX
.antMatchers("main.html").hasAnyRole("A")
//下面为权限级别的控制
.antMatchers("manage.html").hasAnyAuthority("admin,normal")
//只有以下ip才能访问下面的页面
.antMatchers("main.html").hasIpAddress("198.159.3.1");
//csrf防护
http.csrf().disable();
}
}
自定义响应状态处理方案
1.定义AccessDeniedHandle
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置需要处理的响应状态码
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//不然打印不出中文
response.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer=response.getWriter();
writer.print("输出文字");
writer.flush();
writer.close();
}
}
2.在WebSecurityConfig中引用
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandle myaccessDeniedHandle;
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
//设置自定义异常处理方案
http.exceptionHandling().accessDeniedHandler(myaccessDeniedHandle);
//......
}
结合access自定义方法实现权限控制
1.创建对应权限控制方法,逻辑自定
@Service
public class PermissionHandle{
//该方法的意思为loadByUserDetail返回的user中包含的权限必须包含可以访问的网址和角色
//即 所包含的网址 允许 所包含的角色访问
//return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_B,ROLE_A,main.html,manage.html"));
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
Object obj = authentication.getPrincipal();
if (obj instanceof UserDetails){
UserDetails userDetails = (UserDetails) obj;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
2.在WebSecurityConfig中覆盖原有对于权限判断的代码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowire
PermissionHandle permissionHandle;
//......
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
http.authorizeRequests()
.anyRequest().access("@permissionHandle.hasPermission(request,authentication)");
//......
}
//......
}
角色权限判断
1.@Secured注解
@Secured注解是springsecurity提供的,不用导入额外依赖,用于判断角色权限对于方法的访问
启动类添加@EnableGlobalMethodSecurity(securedEnabled = true),默认为false
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
//用于开启SpringSecurity提供的隐藏注解
public class OauthDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OauthDemoApplication.class, args);
}
}
在方法或类上添加@Secured注解,用于判断可以访问该方法的角色(角色在loadUserByUserName中返回时赋予),以 ’ROLE_‘ 开头。
@Controller
public class controller {
//......
@GetMapping("/v1/id")
@Secured("ROLE_ABC")
public String findUserId(){
String id ="xxxxxx";
return id;
}
}
2.@PreAuthorize/@PostAuthorize注解
与上面的@Secured注解功能类似,但是大多情况下都是使用这两个注解,@PreAuthorize用于执行前的判断权限,而@PostAuthorize用于执行后的判断权限
启动类添加@EnableGlobalMethodSecurity(prePostEnabled = true),默认为false
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
//用于开启SpringSecurity提供的隐藏注解
public class OauthDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OauthDemoApplication.class, args);
}
}
在方法或类上添加@PreAuthorize/PostAuthorize注解,功能与@Secured同理,参数为hasRole('XXX')
@Controller
public class controller {
//......
@GetMapping("/v1/id")
//@Secured("ROLE_ABC")'
//与配置类中的hasROle不同的是:允许定于角色可以不以ROLE_开头
@PreAuthorize("hasRole('ABC')")
public String findUserId(){
String id ="xxxxxx";
return id;
}
}
RememberMe功能
SpringSecurity中 RememberMe 为”记住我”功能,用户只需要在登录时添加 remember-me复框,取值为true。SpringSecurity 会自动把用户信息存储到数据源中,以后就可以不登录进行访问
1.由于需要将用户信息存储数据库中,故需要引入持久层依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
2.在application.yml中设置对应持久层配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/security
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
3.在webSecurityConfig中注册对应的Bean(PersistentTokenRepository )和 在configure中配置rememberMe的对应设置,并引入PersistentTokenRepository
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......
@Autowired
private DataSource dataSource;
@Autowired PersistentTokenRepository persistentTokenRepository;
//(1) 定义PersistentTokenRepository
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,只有第一次启动才需要,第二次需要注释掉
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
//(2)在configure中配置rememberMe
http.rememberMe()
//失效时间,单位为秒
.tokenValiditySeconds(60*15)
//自定义登录逻辑
.userDetailsService(userSecurityService)
//持久层对象
.tokenRepository(persistentTokenRepository);
//csrf防护
http.csrf().disable();
}
//......
}
4.前端添加组件
退出登录
1.在webSecurityConfigurer中配置参数
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
http.logout()
//自定义登出请求
.logoutUrl("/user/logout")
//登出按钮跳转页面
.logoutSuccessUrl("/login.html");
//csrf防护
http.csrf().disable();
}
//......
}
2.前端配置href
csrf防护
上文中的http.csrf().disable();如果没有这个则会导致用户无法被认证。这行代码的含义是:关闭csrf防护。 CSRF为跨域请求伪造,通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议、ip地址、端口中任意一项不相同则为跨域请求。
从SringSecurity4.0开始,CSRF防护为默认开启,在部分开发过程中则会要求关闭默认防护,访问时携带参数名为_csrf,值为token(服务器产生)的内容,如果token和服务器记录的token一致则匹配成功,则访问成功。
2024.03.18