Spring Security入门教程
什么是权限管理
权限管理中有主体、认证、授权。
主体:系统的使用者(一般为用户实体类)。
认证:确认主体的身份。
授权:将系统中的权限功能授予主体。
什么是RBAC权限模型
RBAC模型(Role-Based Access Control:基于角色的访问控制)。
非常经典,易用通用的一种权限模型。
这篇文章详细的介绍了RBAC权限模型。(侵权删)
https://blog.csdn.net/m0_60386582/article/details/124165658
什么是Spring Security
一般来说,Web应用的安全性包括用户认证和用户授权两个部分。
Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。Spring Security 的重要核心功能就是解决用户认证和用户授权。
与Shiro对比
- Spring Security特点
1、基于spring开发,所以更契合spring;
2、全面的权限控制;
3、专门为 Web 开发而设计,旧版本不能脱离 Web 环境使用,新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离 Web 环境;
4、更好的支持OAuth2;
5、社区资源比Shiro丰富;
6、Spring Boot对于Spring Security 提供了自动化配置方案;
7、Spring Boot/Spring Cloud环境中,更容易集成Spring Security;
8、重量级; - Shiro特点
1、轻量级;
2、通用性,不需要web环境,但是特定需求需要手动写控制代码;
Spring Security原理
Spring Security其实是一系列过滤器。学过JavaWeb基础就知道三大组件:Servlet、Filter(过滤器)、Listener(监听器)。
其中包括:
- 登录过滤器UsernamePasswordAuthenticationFilter(拦截来自post请求的登录信息,检验表单中的用户名和密码),部分源码如下:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
- 权限过滤器FilterSecurityInterceptor(方法级权限过滤器,基本位于过滤链的最底部),部分源码如下:
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
private boolean isApplied(FilterInvocation filterInvocation) {
return (filterInvocation.getRequest() != null)
&& (filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null);
}
- 异常过滤器ExceptionTranslationFilter(处理在认证授权过程中抛出的异常),部分源码如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
还有很多过滤器,就不一一列举了。
Spring Security实现登录认证
- 默认登录。如果没有配置用户登录信息,Spring Security会自动生成用户账号和密码
账号一般为user,密码会在控制台打印出来,例如:
Using generated security password:1e61d022-7d00-45f0-9909-5ea4a41eea4e
- 配置文件中设置用户登录信息,例如:
#只需要在启动配置文件里设置即可
spring.security.user.name=admin
spring.security.user.password=admin
- 配置类中设置用户登录信息,例如:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
其中PasswordEncoder接口是Spring Security官方推荐的密码解析器,源码如下:
public interface PasswordEncoder {
/*表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,
则返回 true;如果不匹配,则返回 false。
第一个参数表示需要被解析的密码,第二个参数表示存储的密码*/
String encode(CharSequence var1);
/*表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 false*/
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
- 自定义编写实现类设置用户登录信息(常用读表中用户信息)
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
.loginPage("/index.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login.html") // 设置哪个是登录的 url。
.successForwardUrl("/success.html") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail.html");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/layui/**","/index.html") //表示配置请求路径
.permitAll() // 指定 URL 无需保护。
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭 csrf
http.csrf().disable();
}
- Service实现UserDetailsService 接口,并进行读取数据操作,返回一个UserDetails实体类,这个类是系统默认用户的主体,例如:
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper(); //Mybatis-plus
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if(users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
//权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
//返回UserDetails实体类
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
Spring Security实现权限管理
- hasAuthority 方法设置权限
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
.loginPage("/index.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login.html") // 设置哪个是登录的 url。
.successForwardUrl("/success.html") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail.html");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/layui/**","/index.html") //表示配置请求路径
.hasAuthority("admin") //表示需要admin权限才能访问index.html
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭 csrf
http.csrf().disable();
}
- hasAnyAuthority 方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true。 - hasAnyRole方法
表示用户具备任何一个条件都可以访问。 - hasRole方法
如果用户具备给定角色就允许访问,否则出现 403。如果当前主体具有指定的角色,则返回 true。
注意ROLE_拼接 - 基于数据库实现权限管理(RBAC权限模型)
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper(); //Mybatis-plus
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if(users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
//权限
List<Role> rolelist = usersMapper.selectRoleByUserId(users.getId());
List<Menu> menulist = usersMapper.selectMenuByUserId(users.getId());
List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
for(Role role : rolelist){
SimpleGrantedAuthority authorityRole = new SimpleGrantedAuthority("ROLE",role.getName());
auths.add(authorityRole);
}
for(Menu menu : menulist){
SimpleGrantedAuthority authorityMenu = new SimpleGrantedAuthority(menu.getPermissions());
auths.add(authorityMenu);
}
//返回UserDetails实体类
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
.loginPage("/index.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login.html") // 设置哪个是登录的 url。
.successForwardUrl("/success.html") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail.html");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/layui/**","/index.html") //表示配置请求路径
.hasRole("管理员")
.hasAuthority("menu:add") //表示需要admin权限才能访问index.html
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭 csrf
http.csrf().disable();
}
- 权限相关注解
- @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀"ROLE_"
使用方法:- 在启动类上开启注解功能:@EnableGlobalMethodSecurity(securedEnabled=true)
- 在控制起方法上添加注解:@Secured({“ROLE_normal”,“ROLE_admin”})
- @PreAuthorize
注解适合进入方法前的权限验证,另以将登录用户的 权限参数传到方法中
使用方法:- 在启动类上开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled=true)
- 在控制层方法上添加注解:@PreAuthorize(“hasAnyAuthority(‘menu:add’)”)
- @PostAuthorize
在方法执行后再进行权限验证,适合验证带有返回值的权限
使用方法:- 在启动类上开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled=true)
- 在控制层方法上添加注解:@PostAuthorize(“hasAnyAuthority(‘menu:add’)”)
- @PostFilter
权限验证之后根据用户过滤数据
使用方法:- 在控制层方法上添加注解:@PostFilter(“filterObject.username == ‘用户名’”)
- 表达式中的 filterObject引用的是注解方法返回值集合中的某一个变量元素
- @PreFilter
- 进入控制层之前对数据进行过滤*
使用方法:- 在控制层方法上添加注解:@@PreFilter(value = “filterObject.id%2==0”)
- 表达式中的 filterObject引用的是注解方法返回值集合中的某一个变量元素
- @Secured
总结
Spring Security过程
1、用户登录,传用户信息参数
2、获取到的用户账号和密码封装成一个UsernamePasswordAuthenticationToken,实现 Authentication接口。
3、将UsernamePasswordAuthenticationToken给AuthenticationManager进行登录认证。
4、认证成功后会返回一个Authentication 对象,封装了用户信息。
5、调用 SecurityContextHolder.getContext().setAuthentication将AuthenticationManager返回的 Authentication对象赋予给当前的SecurityContext。