Springboot 整合 Spring Security
Springboot 整合 Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
项目最终目录结构
一.Spring Security 引入
1.pom 文件引入 spring security 依赖
<!-- spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.启动项目,并在浏览器访问项目,出现如下登录页,说明 security 已引入成功
这是 security 自带的默认登录页面,项目的操作需要登录后才可进行操作
用户名默认为 user ,密码可在项目启动日志中可进行查看
二.自定义用户
Spring Security 支持三种用户配置方式
1.通过配置文件方式
在 application.yml 中添加用户
spring: # security security: user: #用户名 name: ppp #密码 password: 123456 #权限 roles: USER
2.基于内存方式
//认证 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("aaa").password("123").roles("USER") .and() .withUser("admin").password("123").roles("ADMIN","USER") .and() .withUser("bbb").password("123").roles("ADMIN"); }
3.基于数据库方式
真正使用中,用得最多的还是基于数据库方式,所以这里主要讲解第三种方式。
为方便操作数据库,项目中已整合 mybatis-plus。如小伙伴未整合有需要,可参考我以前发布过的文章“Springboot 整合 mybatis-plus”。
数据库中创建用户表
create table tbl_user ( id int auto_increment primary key, user_code varchar(200) not null, password varchar(2000) not null, role varchar(2000) null );
向表中添加用户数据
创建 security 授权配置文件 WebSecurityConfig
import com.example.demo.web.service.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity //开启注解功能 @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) // 启用方法级别的权限认证 public class WebSecurityConfig { @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; /*@Autowired private JwtAuthError jwtAuthError; // jwt 校验过滤器,从 http 头部 Authorization 字段读取 token 并校验 @Bean public JwtAuthFilter authFilter() throws Exception { return new JwtAuthFilter(); }*/ /** * 密码明文加密方式配置 * @return */ @Bean public PasswordEncoder passwordEncoder() { //无加密 //return NoOpPasswordEncoder.getInstance(); return new BCryptPasswordEncoder(); } /** * 获取AuthenticationManager(认证管理器),登录时认证使用 * @param authenticationConfiguration * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // 基于 token,不需要 csrf .csrf().disable() // 基于 token,不需要 session //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 设置 jwtAuthError 处理认证失败、鉴权失败 // .exceptionHandling().authenticationEntryPoint(jwtAuthError).accessDeniedHandler(jwtAuthError).and() // 下面开始设置权限 .authorizeRequests(authorize -> authorize // 请求放开 .antMatchers("/static/**", "/login", "/hello/").permitAll() .antMatchers("/demo/test").hasAuthority("admin") .antMatchers("/demo/test1").hasAnyAuthority("user", "op") // 其他地址的访问均需验证权限 .anyRequest().authenticated() ) .formLogin() .loginPage("/login") //登陆请求处理接口 .loginProcessingUrl("/login") //用户名文本框 name属性 .usernameParameter("username") //密码文本框 name属性 .passwordParameter("password") //如果登录成功会跳转到"/hello" .defaultSuccessUrl("/index") //登录成功处理 // .successHandler((req, resp, auth) -> { // // }) //如果登录失败会跳转到"/hello" .failureForwardUrl("/hello") //登录失败处理 // .failureHandler((req, resp, e) -> { // // }) .and() //登出配置 .logout() .logoutUrl("/admin/logout") //指定登出的地址,默认是"/logout" .logoutSuccessUrl("/hello") //登出后的跳转地址 .clearAuthentication(true) //清楚身份信息 .invalidateHttpSession(true) //session 失效,默认为true .deleteCookies("usernameCookie","urlCookie") //在登出同时清除cookies // //自定义LogoutSuccessHandler,在登出成功后调用,如果被定义则logoutSuccessUrl()就会被忽略 // .logoutSuccessHandler((req, resp, auth) -> {//注销成功处理 // resp.sendRedirect("/login_page"); //跳转到自定义登陆页面 // }) //添加自定义的LogoutHandler,默认会添加SecurityContextLogoutHandler // .addLogoutHandler((req, resp, auth) -> {//注销处理 // // }) .and() //自定义403页面 .exceptionHandling() .accessDeniedPage("/403") .and() // 添加 JWT 过滤器,JWT 过滤器在用户名密码认证过滤器之前 //.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class) // 认证用户时用户信息加载配置,注入springAuthUserService .userDetailsService(userDetailsServiceImpl) .build(); } /** * 配置跨源访问(CORS) * @return */ @Bean CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
hasRole() 当前用户是否拥有指定角色。
hasAnyRole() 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
hasAuthority() 等同于hasRole
hasAnyAuthority() 等同于hasAnyRole
hasRole 的处理逻辑和 hasAuthority 类似,不同的是,hasRole 这里会自动给传入的字符串加上 ROLE_ 前缀,所以在数据库中的权限字符串需要加上 ROLE_ 前缀。即数据库中存储的用户角色如果是 ROLE_admin,这里就是 admin。
创建 UserDetailsServiceImpl 自定义身份认证
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.demo.web.entity.AdminUserEntity; import com.example.demo.web.mapper.AdminUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private AdminUserMapper adminUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("username", username); // MOCK 模拟从数据库 根据用户名查询用户 AdminUserEntity account = adminUserMapper.selectOne(wrapper); if (account == null) { throw new UsernameNotFoundException("用户不存在"); } //用户权限 String[] permissionArray = account.getRole().split(","); //return User.withUsername(account.getUsername()).password(new BCryptPasswordEncoder().encode(account.getPassword())).authorities(new SimpleGrantedAuthority(permissionArray)).build(); return User.withUsername(account.getUsername()).password(account.getPassword()).authorities(permissionArray).build(); } }
创建 AdminUserEntity
@Data @NoArgsConstructor @AllArgsConstructor @TableName("yz_admin_user") public class AdminUserEntity { private Integer id; private String username; private String password; private String role; }
创建 AdminUserMapper
public interface AdminUserMapper extends BaseMapper{ }
测试启动项目,在登录页输入数据库中的用户名和密码登录
登录成功,并跳转到项目主页
登录 java用户,因为 java用户只有 USER 权限,所以访问 demo/test 时是没权限的,所以出现 403 无权限错误页面
security 连接数据库登录完成。
三.加密策略
刚刚的写法,用户的密码是没有进行加密的。
真实项目使用中,为了保护用户的信息安全,数据库中的密码是必须进行加密保存。
更改 security 加密策略
//密码加密方式 @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
修改数据库用户密码
Spring Security 提供了丰富的加密策略
配置完成,启动项目测试
授权配置完成
四.自定义登录页
在项目中添加登录页 html,以及访问 controller 。
controller
package com.example.demo.common.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; @Slf4j @Controller public class CommonController { @GetMapping(value = "hello") public ModelAndView hello(){ return new ModelAndView("login"); } @GetMapping(value = "index") public ModelAndView index(){ return new ModelAndView("index"); } }
remember-me 记住我设置
数据库添加表 persistent_logins,用于存放token
create table persistent_logins ( username varchar(64) not null, series varchar(64) not null primary key, token varchar(64) null, last_used timestamp default CURRENT_TIMESTAMP not null );
security 配置文件注入数据源 DataSource 和配置 TokenRepository ,需要和数据库进行交互
//注入数据源 @Autowired private DataSource dataSource; private PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //第一次启动的时候自动建表 // jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; }
security 配置文件中 configure(HttpSecurity http) 添加配置
.and() //记住我配置 .rememberMe() //记住我复选框框 name属性 .rememberMeCookieName("remember-me") .tokenRepository(persistentTokenRepository()) //设置有效时长,单位秒 .tokenValiditySeconds(60) .userDetailsService(myUserDetailsService)
最终 security 配置文件
package com.example.demo.web.config; import com.example.demo.web.service.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证 public class WebSecurityConfig { @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; /*@Autowired private JwtAuthError jwtAuthError; // jwt 校验过滤器,从 http 头部 Authorization 字段读取 token 并校验 @Bean public JwtAuthFilter authFilter() throws Exception { return new JwtAuthFilter(); }*/ /** * 密码明文加密方式配置 * @return */ @Bean public PasswordEncoder passwordEncoder() { //无加密 //return NoOpPasswordEncoder.getInstance(); return new BCryptPasswordEncoder(); } /** * 获取AuthenticationManager(认证管理器),登录时认证使用 * @param authenticationConfiguration * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // 基于 token,不需要 csrf .csrf().disable() // 基于 token,不需要 session //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 设置 jwtAuthError 处理认证失败、鉴权失败 // .exceptionHandling().authenticationEntryPoint(jwtAuthError).accessDeniedHandler(jwtAuthError).and() // 下面开始设置权限 .authorizeRequests(authorize -> authorize // 请求放开 .antMatchers("/static/**", "/login", "/hello/").permitAll() .antMatchers("/demo/test").hasAuthority("admin") .antMatchers("/demo/test1").hasAnyAuthority("user", "op") // 其他地址的访问均需验证权限 .anyRequest().authenticated() ) .formLogin() .loginPage("/hello") //登陆请求处理接口 .loginProcessingUrl("/login") //用户名文本框 name属性 .usernameParameter("username") //密码文本框 name属性 .passwordParameter("password") //如果登录成功会跳转到"/hello" .defaultSuccessUrl("/index") //登录成功处理 // .successHandler((req, resp, auth) -> { // // }) //如果登录失败会跳转到"/hello" .failureForwardUrl("/hello") //登录失败处理 // .failureHandler((req, resp, e) -> { // // }) .and() //登出配置 .logout() .logoutUrl("/admin/logout") //指定登出的地址,默认是"/logout" .logoutSuccessUrl("/hello") //登出后的跳转地址 .clearAuthentication(true) //清楚身份信息 .invalidateHttpSession(true) //session 失效,默认为true .deleteCookies("usernameCookie","urlCookie") //在登出同时清除cookies // //自定义LogoutSuccessHandler,在登出成功后调用,如果被定义则logoutSuccessUrl()就会被忽略 // .logoutSuccessHandler((req, resp, auth) -> {//注销成功处理 // resp.sendRedirect("/login_page"); //跳转到自定义登陆页面 // }) //添加自定义的LogoutHandler,默认会添加SecurityContextLogoutHandler // .addLogoutHandler((req, resp, auth) -> {//注销处理 // // }) .and() //自定义403页面 .exceptionHandling() .accessDeniedPage("/403") .and() //记住我配置 .rememberMe() //记住我复选框框 name属性 .rememberMeCookieName("remember-me") .tokenRepository(persistentTokenRepository()) //设置有效时长,单位秒 .tokenValiditySeconds(60) .and() // 添加 JWT 过滤器,JWT 过滤器在用户名密码认证过滤器之前 //.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class) // 认证用户时用户信息加载配置,注入springAuthUserService .userDetailsService(userDetailsServiceImpl) .build(); } /** * 配置跨源访问(CORS) * @return */ @Bean CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
五.security 注解权限控制
想要使用 security 注解,需要在启动类或配置文件中添加注解 @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) 开启security 注解功能
其中:
prePostEnabled = true 开启 @PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter 注解
securedEnabled = true 开启 @Secured 注解
1). @Secured @GetMapping("/test") @Secured({"ADMIN","USER"}) public String test() { return "hello"; }
说明:拥有ADMIN或者USER角色的用户都可以方法 test() 方法。
2). @PreAuthorize
在方法执行前进行权限验证
与 @Secured 类似,但是 @PreAuthorize 支持 SpEL表达式
@GetMapping("/test") @PreAuthorize("hasAuthority('ADMIN','USER')") public String test() { return "hello"; }
说明:拥有normal或者admin角色的用户都可以方法 test()方法。
此时如果我们要求用户必须同时拥有normal和admin的话,可以这么写
@GetMapping("/test") @PreAuthorize("hasAuthority('ADMIN') AND hasAuthority('USER')") public String test() { return "hello"; }
3). @PostAuthorize
在方法执行后再进行权限验证
@PostAuthorize("returnObject %2 == 0") @GetMapping(value = "test") public int test(){ int i=new Random().nextInt(10); System.out.println(i); return i; }
@PostAuthorize 在方法调用完之后进行权限、数据结果检查
4). @PreFilter
@PreFilter 可以对集合类型的参数进行过滤。使用@PreFilter时,Spring Security将移除使对应表达式的结果为false的元素。
@PreFilter(value = "filterObject ==1") @GetMapping(value = "test") public Listtest(List list){ return list; }
5). @PostFilter
@PostFilter可以对集合类型的返回值进行过滤。使用@PostFilter时,Spring Security将移除使对应表达式的结果为false的元素。
@PostFilter(value = "filterObject==1") @GetMapping(value = "test") public Listtest(){ List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); return list; }
常用SpringSecurity的标签属性介绍
sec:authorize=”isAuthenticated()”
判断用户是否已经登陆认证,引号内的参数必须是isAuthenticated()。
sec:authentication=“name”
获得当前用户的用户名,引号内的参数必须是name。
sec:authorize=“hasAuthority(‘role’)”
判断当前用户是否拥有指定的权限。引号内的参数为权限的名称。
sec:authentication=”principal.authorities”
获得当前用户的全部角色,引号内的参数必须是principal.authorities。
HttpSecurity 常用方法及说明
方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应
cors() 配置跨域资源共享( CORS)
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterAt() 在指定的Filter类的位置添加过滤器
Springboot 整合 Spring Security 完成。