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 完成。

