A092_SpringSecurity
目录
- 一.认证与授权-新手上路
- 1.认证授权概述
- 1.1.什么是认证
- 1.2.什么是授权
- 1.3.什么是RBAC
- 2.基于session的认证授权
- 2.1.认证流程和认证检查
- 2.2.授权流程
- 3.基于RBAC实现认证授权
- 3.1.认证逻辑
- 3.2.授权逻辑
- 3.3.小结
- 4. 单词汇总
- 二.SpringSecurity-小试牛刀
- 1.SpringSecurity概述
- 1.1.SpringSecurity介绍
- 2.SpringSecurity入门
- 2.1.入门案例-认证
- 2.2.认证流程小结
- 三.认证流程-登堂入室
- 1.认证流程原理
- 1.1.认证授权流程
- 1.2.Security过滤器链
- 1.3.Security相关概念
- 1.4.SpringSecurity认证流程原理
- 2.定义认证流程
- 2.1.定义UserDetailsService
- 2.2.MyBatis集成
- 2.3.定义密码编码器
- 2.4.自定义登录页面
- 2.5.自定义登出
- 四.授权流程-游刃有余
- 1.授权流程原理
- 1.1.授权流程描述
- 2.Web授权
- 2.1.web授权API说明
- 2.2.Web授权实战
- 3.方法授权
- 3.1.@Secured
- 3.2.@PreAuthorize
- 3.3.@PostAuthorize
- 五.认证结果处理-登峰造极
- 1.认证成功处理
- 1.1.解决方案
- 1.2.认证成功结果处理
- 2.认证失败结果处理
- 2.1.解决方案
- 2.2.认证失败结果处理
- 六.授权结果处理-炉火纯青
- 1.授权失败处理
- 1.1.概述
- 1.2.定义认证检查失败处理
- 七.记住我-走火入魔
- 1.记住我概述
- 1.1.什么是记住我
- 1.2.流程分析
- 2.编码实战
- 2.1.修改该登录页
- 2.2.配置TokenRepository
- 2.3.配置rememberMe
- 2.4.创建persistent_logins 的表
- 2.5.测试
- 扩展
一.认证与授权-新手上路
1.认证授权概述
1.1.什么是认证
认证是对主体/用户身份的确认,在我们的生活中随处可见认证场景,如:小区门禁卡,人脸识别,指纹识别等都是对用户身份的确认,在传统的应用中我们通常使用用户名/用户ID和密码来进行用户的身份确认,即登录,但登录的方式不仅限制于用户名/密码的方式,认证是我们应用的第一道安全门,所以对于整个系统的安全来说显得极其重要。
1.2.什么是授权
控制不同的用户访问不同的权限 ,用户认证成功后,就可以对某些资源进行访问,但是不同的用户有不同的资源访问权利,那么对用户的授权也都不一样。如:公司老板拥有对公司的所有权限,而部门主管只能有对自己管理的部门的权限,所以在程序中授权的过程就是赋予不同用户不同权限的过程。
1.3.什么是RBAC
RBAC是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
举例:可以把部门的展示,添加,修改,删除等资源或功能打包成角色“部门管理”,然后把“部门管理”这个角色赋予某个用户,那么这个用户就是部门管理员,拥有部门的相关权限。
RBAC 认为授权实际上是Who 、What 、How 三元组之间的关系,也就是Who 对What 进行How 的操作。
主体(Who ) :是权限的拥有者或主体(如:User,Role)。
资源 (what): 是操作或对象,如:页面,菜单,按钮,控制器(controller,hanler)等
权限(how) :具体的权限, 如:张三可以删除员工。 那么删除员工就是一个权限
2.基于session的认证授权
2.1.认证流程和认证检查
基于Session的认证方案即当用户认证成功之后将认证信息缓存在session中,主体(用户)在发起资源访问时需要进行是否做过认证校验,即检查session中是否有缓存认证信息。详细的认证流程如下图:
认证流程
客户端(浏览器)提交用户名和密码发起认证请求
请求中的认证信息被封装成对象(User),控制层(controller)接收到认证请求
Controller调用服务层(Service)的认证逻辑进行认证,传入User
Service调用持久层(Dao)根据传入的User中的账号查询数据库中的认证信息(User)
Service获取到持久层返回的User和请求传入的User进行密码比对
认证成功将认证信息(User)和用户的权限信息通过UserContext存储到(Session中)
返回认证结果给客户端
认证检查
当用户再次发起请求,拦截器会负责检查是否已经完成认证,通过UserContext从Session中获取User对象进行判断,如果没认证就会返回错误信息,如果已经完成认证就直接访问url对应的资源,返回资源给客户端。
2.2.授权流程
授权是约束用户对资源的访问权限,通过系统事先需要对资源做好授权操作,即约定好哪些资源需要什么样的权限才能访问。并且平台的用户需要被分配好其拥有的权限(用户/角色/权限) , 在用户认证成功之后即为用户加载其拥有的权限列表和认证信息一起封装成对象缓存到session中 ,当用户发起资源请求时,除了需要做认证检查还需要做权限检查,即判断当前认证用户所拥有的权限列表中是否拥有当前访问的资源所需要的权限。详细授权流程如下:
认证通过,加载当前用户的权限列表,和认证信息一起封装成对象存储到session中
服务端登录检查拦截器,校验是否已经认证(从session获取认证信息)
服务端权限检查拦截器,校验是否拥有访问权限(用户的权限列表是否包含资源所需要的权限)
3.基于RBAC实现认证授权
3.1.认证逻辑
代码回顾 ; 省略… 见代码:resources/beike-spring-security-parent/authentication-rbac
3.2.授权逻辑
代码回顾 ; 省略… 见代码:resources/beike-spring-security-parent/authentication-rbac
3.3.小结
传统的认证授权方案无疑太麻烦,在企业开发中往往会借助一些三方认证工具进行认证和授权,如:Shiro,SpringSecurity等,这类认证工具在传统的认证授权的基础上进行封装,让认证授权流程更简单,并且提供了一些强大的功能,如会话管理,缓存等。我们接下来要研究的就是背靠Spring家族的安全框架SpringSecurity。
4. 单词汇总
Authorization :授权
Authentication :认证(登录)
WebSecurityConfigurerAdapter : web安全配置类(认证授权配置)
@EnableWebSecurity :开启web安全配置
HttpSecurity : http安全配置
permitAll : 放行
antMatchers : 用来匹配某些url路径
UserDetailsService :用户详情服务(用来加载数据库的用户数据) UserDao
SecurityContext : Security上下文工具,保存得有认证成功之后的用户信息
二.SpringSecurity-小试牛刀
1.SpringSecurity概述
1.1.SpringSecurity介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
2.SpringSecurity入门
2.1.入门案例-认证
搭建工程
基于SpringBoot搭建web工程 ,项目名为“spring-security-demo”
导入依赖
这里继承了SpringBoot的父工程,引入SpringSecurity基础依赖
“spring-boot-starter-security”,以及集成web的依赖“spring-boot-starter-web”
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.5.RELEASE</version>
</parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency></dependencies>
主配置类
@SpringBootApplication
public class ApplicationConfig {public static void main(String[] args) {SpringApplication.run(ApplicationConfig.class);}
}
Web控制器
当用户认证成功之后会重定向到该方法,返回“登录成功”给用户
@Controller
public class AuthController {//登录成功后重定向地址@RequestMapping("/loginSuccess")@ResponseBodypublic String loginSuccess(){return "登录成功";}
}
配置SpringSecurity
SpringSecurity提供了一个配置类WebSecurityConfigurerAdapter用来提供给程序员对SpringSecurity做自定义配置,我们需要配置如下几个信息:
创建UserDetailService的Bean,该组件是用来加载用户认证信息
配置编码器,通过该编码器对密码进行加密匹配。
授权规则配置,哪些资源需要什么权限…
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());return inMemoryUserDetailsManager;}//密码编码器:不加密@Beanpublic PasswordEncoder passwordEncoder(){return NoOpPasswordEncoder.getInstance();}//授权规则配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests() //授权配置.antMatchers("/login").permitAll() //登录路径放行.anyRequest().authenticated() //其他路径都要认证之后才能访问.and().formLogin() //允许表单登录.successForwardUrl("/loginSuccess") // 设置登陆成功页.and().logout().permitAll() //登出路径放行 /logout.and().csrf().disable(); //关闭跨域伪造检查}
}
2.2.认证流程小结
SpringBoot+SpringSecurity集成入门案例到这里就结束了,这里并没有多SpringSecurity原理做过多解释(留在后面章节),那么这个案例大概的实现思路是:
SpringSecurity根据我们在WebSecurityConfig中的配置会对除了“/login”之外的资源进行拦截做认证检查,
如果没有认证会跳转到默认的认证页面“/login” ,
输入用户名和密码后点击登录,SpringSecurity会带着用户名调用 UserDetailsService.loadUserByUsername获取用户的认证信息(用户名,密码,权限等),
然后执行认证工作:表单密码和loadUserByUsername加载的数据库的密码进行匹配(PasswordEncoder)
认证成功跳转成功地址
三.认证流程-登堂入室
1.认证流程原理
1.1.认证授权流程
SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。流程如下图:
1.2.Security过滤器链
我们知道,SpringSecurity是通过很多的过滤器链共同协作完成认证,授权的流程,SpringSecurity中核心的过滤器链如下:
SecurityContextPersistenceFilter
这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中获取SecurityContext对象并设置给SecurityContextHolder。在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext
UsernamePasswordAuthenticationFilter
默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证。
BasicAuthenticationFilter 基本认证,httpBasic登录,弹出登录框登录
RememberAuthenticationFilter 记住我
AnonymousAuthenticationFilter
匿名Filter,用来处理匿名访问的资源,如果SecurityContext中没有Authentication,就会创建匿名的Token(AnonymousAuthenticationToken),然后通过SecurityContextHodler设置到SecurityContext中。
ExceptionTranslationFilter
用来捕获FilterChain所有的异常,进行处理,但是只会处理AuthenticationException和AccessDeniedException异常,其他的异常会继续抛出。
FilterSecurityInterceptor
用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用AccessDecisionManager.decide方法对用户进行授权。
1.3.Security相关概念
AuthenticationToken
所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码。
AuthenticationManager
用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会 调
用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证
信息的Authentication对象。
AuthenticationProvider.authenticate
认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。
UserDetailService
用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。
SecurityContext
当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的
1.4.SpringSecurity认证流程原理
请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)
过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证.
AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较
AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回
Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后UsernamePasswordAuthenticationFilter调用AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作
最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到SecurityContext对象然后调用SecurityContextRepository将SecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。
注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息。
2.定义认证流程
在SpringSecurity的整个认证流程中,除了UserDetailsService需要我们自己定义外,其他的的组件都可以使用默认的,因为UserDetailsService是SpringSecurity获取数据库中的认证信息的媒介,而如何才能从数据库中获取认证信息只有我们才知道。在入门案例中我们使用的是InMemoryUserDetailsManager 基于内存的UserDetailsService方案,接下来我们需要把基于内存的方案修改为基于数据库的方案。
2.1.定义UserDetailsService
相关概念
UserDetailsService
是SpringSecurity提供用来获取认证用户信息(用户名,密码,用户的权限列表)的接口,我们可以实现该接口,复写loadUserByUsername(username) 方法加载我们数据库中的用户信息
UserDetails
UserDetails是SpringSecurity用来封装用户认证信息,权限信息的对象,我们使用它 的实现类User封装用户信息 并返回,我们这里从数据库查询用户名
基于入门案例进行修改
准备好认证表t_login(密码密文) ,集成MyBatis等,做好准备工作
创建类UserDetailServiceImpl实现UserDetailsService接口
/*** 用来提供给security的用户信息的service,* 我们需要复写 loadUserByUsername 方法返回数据库中的用户信息*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {/*** 加载数据库中的认证的用户的信息:用户名,密码,用户的权限列表* @param username: 该方法把username传入进来,我们通过username查询用户的信息(密码,权限列表等)然后封装成 UserDetails进行返回 ,交给security 。*/@Overridepublic UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {cn.itsource.domain.User userFromMysql = userMapper.selectByUsername(username);if(loginFromMysql == null){throw new UsernameNotFoundException("无效的用户名");}List<GrantedAuthority> permissions = new ArrayList<>();//密码是基于BCryptPasswordEncoder加密的密文//User是security内部的对象,UserDetails的实现类 ,//用来封装用户的基本信息(用户名,密码,权限列表)return new User(username,loginFromMysql.getPassword(),permissions);}
}
Provider会调用UserDetailsService 获取认证信息,这里自定义的UserDetailsService实现类,复写了loadUserByUsername方法,根据用户名查询数据库中的认证信息和当前用户的权限信息,封装成User返回。
注意:这里定义了UserDetailSerice后,WebSecurityConfig中不在需要定义UserDetailService的Bean需要移除
2.2.MyBatis集成
1.导入依赖
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.20</version></dependency><!-- mysql 数据库驱动. --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.1.1</version></dependency>
2.配置MyBatis
spring:datasource:url: jdbc:mysql:///auth-rbacusername: rootpassword: admindriver-class-name: com.mysql.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSource
mybatis:mapper-locations: classpath:cn/itsource/rbac/mapper/*Mapper.xml
@SpringBootApplication
@MapperScan("mapper接口包")
public class ApplicationConfig {public static void main(String[] args) {SpringApplication.run(ApplicationConfig.class);}
}
2.编写Mapper.xml
3.编写Mapper映射器接口
4.创建数据库 :sql见资料:auth-rbac.sql
2.3.定义密码编码器
在我们的案例中,密码一值是明文的,我们指定的密码编码器是 NoOpPasswordEncoder ,这个是不加密的,但是在生产环境中我们数据库中的密码肯定是密文,所以我们需要指定密码的编码器,那么SpringSecurity在认证时会调用我们指定的密码编码器进行认证
BCryptPasswordEncoder
BCryptPasswordEncoder是SpringSecurity内部提供的编码器,他的好处在于多次对相 同的明文加密出来的密文是不一致的,但是多次加密出来的不同密文确有能检查通过, 这种方式增加了密码的安全性,测试代码如下:
public class PasswordTest {@Testpublic void testPassword(){BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String enPass = bCryptPasswordEncoder.encode("123");System.out.println(enPass);System.out.println(bCryptPasswordEncoder.matches("123", enPass));}
}
在配置类中定义编码器如下:
@Bean
public PasswordEncoder passwordEncoder(){//return NoOpPasswordEncoder.getInstance();return new BCryptPasswordEncoder();
}
重启测试
2.4.自定义登录页面
准备登录页面static/login.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login"><div>用户名:<input type="text" name="username"></div><div>密码:<input type="password" name="password"></div><div><button type="submit">立即登陆</button></div>
</form>
</body>
</html>
配置登录页面
在WebSecurityConfig配置类中配置登陆页面和登陆请求地址
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/login").permitAll() //登录路径放行.antMatchers("/login.html").permitAll() //对登录页面跳转路径放行.anyRequest().authenticated() //其他路径都要拦截.and().formLogin() //允许表单登录, 设置登陆页.successForwardUrl("/loginSuccess") // 设置登陆成功页.loginPage("/login.html") //登录页面跳转地址.loginProcessingUrl("/login") //登录处理地址(必须).and().logout().permitAll(); //登出}
http.csrf().disable() :屏蔽跨域伪造检查
antMatchers("/login.html").permitAll() : 对登录页面跳转路径放行
loginPage("/login.html") :登录页面跳转地址(必须)
loginProcessingUrl("/login") :登录处理地址(必须)
2.5.自定义登出
SpringSecurity提供了默认的退出处理,可以在Security配置类中通过.and().logout().permitAll(); 使用默认的退出路径“/logout” ,如果我们需要自定义退出路径,可以通过如下方式指定:
.and().logout().logoutUrl("/mylogout").permitAll() //制定义登出路径
.logoutSuccessHandler(new MyLogoutHandler()) //登出后处理器-可以做一些额外的事情
.invalidateHttpSession(true); //登出后session无效
四.授权流程-游刃有余
1.授权流程原理
1.1.授权流程描述
授权一定是在认证通过之后,授权流程是通过FilterSecurityInterceptor拦截器来完成,FilterSecurityInterceptor通过调用SecurityMetadataSource来获取当前访问的资源所需要的权限,然后通过调用AccessDecisionManager投票决定当前用户是否有权限访问当前资源。授权流程如下
RBAC传统授权流程:
Security授权流程:
- 在FilterSecurityInterceptor中会调用其父类AbstractSecurityInterceptor
的beforeInvocation方法做授权之前的准备工作 - 该方法中通过SecurityMetadataSource…getAttributes(object);获得资源所需要的访问权限 ,通过SecurityContextHolder.getContext().getAuthentication()获取当前认证用户的认证信息,即Authentication对象 。
- 然后通过调用AccessDecisionManager.decide(authenticated, object, attributes);进行授权,该方法使用了投票机制来决定用户是否有资源访问权限
AccessDecisionManager接口有三个实现类,他们通过通过AccessDecisionVoter投票
器完成投票,三种投票策略如下:
AffirmativeBased : 只需有一个投票赞成即可通过
ConsensusBased:需要大多数投票赞成即可通过,平票可以配置
UnanimousBased:需要所有的投票赞成才能通过 - 而投票器也有很多,如RoleVoter通过角色投票,如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票,AuthenticatedVoter 是用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户(登录后的)
- 投票通过,放心请求响应的资源
2.Web授权
2.1.web授权API说明
在Security配置类中,可以通过HttpSecurity.authorizeRequests()给资源指定访问的权限,其API如下:
- anyRequest():任何请求
- antMatchers(“/path”) :匹配某个资源路径
- authenticationed() : 保护URL需要登录访问anyRequest().authenticationed()
- permitAll():指定url无需保护(放行)一般用户静态资源antMatchers(“/path”).permitAll()
- hasRole(String role):某个资源需要用户拥有什么样的role才能访问
antMatchers(“/path”).hasRole(“admin”) - hasAuthority(String authority):某个资源需要用户拥有什么样的权限才能访问
antMatchers(“/path”).hasAuthority(“admin”) - hasAnyRole(String …roles):某个资源拥有指定角色中的一个就能访问
- hasAnyAuthority(String … authorities):某个资源拥有指定权限中的一个就能访问
- access(String attribute):该方法使用SPEL表达式,可以创建复杂的限制
- hasIpAddress(String ip):拥有什么样的ip或子网可以访问该资源
授权规则注意
我们通常把细节的规则设置在前面,范围比较大的规则设置放在后面,返例:如有以下配置
.antMatchers("/admin/**").hasAuthority(“admin”)
.antMatchers("/admin/login").permitAll();
那么第二个权限规则将不起作用,因为第一个权限规则覆盖了第二个权限规则
因为权限的设置是按照从上到下的优先级。及满足了最开始的权限设置,那么后面的设置就不起作用了。
2.2.Web授权实战
我们这一次在入门案例的基础上进行修改,所有的认证数据,授权数据都从数据库进行获取
1.准备数据库和Domain,mapper等
t_user //用户登录表
t_user_role //用户和角色中间表
t_permisstion //权限表
t_role //角色表
t_role_permission //角色和权限中间表
2.编写controller
@RestController
public class DeptController {@RequestMapping("/dept/list")public String list(){return "dept.list";}@RequestMapping("/dept/add")public String add(){return "dept.add";}@RequestMapping("/dept/update")public String update(){return "dept.update";}@RequestMapping("/dept/delete")public String delete(){return "dept.delete";}
}---------------------------------------------------------
@RestController
public class EmployeeController {@RequestMapping("/employee/list")public String list(){return "employee.list";}@RequestMapping("/employee/add")public String add(){return "employee.add";}@RequestMapping("/employee/update")public String update(){return "employee.update";}@RequestMapping("/employee/delete")public String delete(){return "employee.delete";}
}
方法上的requestmapping就对应了权限表t_permission的资源
3.配置HttpSecurity
@Overrideprotected void configure(HttpSecurity http) throws Exception {List<Permission> permissions = loginMapper.listPermissions();ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistryexpressionInterceptUrlRegistry = http.csrf().disable() //关闭CSRF跨站点请求伪造防护.authorizeRequests() //对请求做授权处理.antMatchers("/login").permitAll() //登录路径放行.antMatchers("/login.html").permitAll();//对登录页面跳转路径放行//动态添加授权:从数据库动态查询出,哪些资源需要什么样的权限for(Permission permission : permissions){System.out.println(permission.getResource()+" - "+permission.getSn());//如: /employee/list 需要 employee:list 权限才能访问expressionInterceptUrlRegistry.antMatchers(permission.getResource()).hasAuthority(permission.getSn());}expressionInterceptUrlRegistry.anyRequest().authenticated() //其他路径都要拦截.and().formLogin() //允许表单登录, 设置登陆页.successForwardUrl("/loginSuccess") // 设置登陆成功页.loginPage("/login.html") //登录页面跳转地址.loginProcessingUrl("/login") //登录处理地址.and().logout().permitAll(); //登出
}
解释:上面代码从权限表查询出了所有的资源(对应controller中的Requestmapping路径),然后通过循环调用expressionInterceptUrlRegistry.antMatchers(permission.getResource())
.hasAuthority(permission.getSn()); 进行一一授权,指定哪个资源需要哪个权限才能访问。
4.修改UserDetailService加载用户权限
public UserDetails loadUserByUsername(String username) {Login loginFromMysql = loginService.selectByUsername(username);if(loginFromMysql == null){throw new UsernameNotFoundException("无效的用户名");}//前台用户List<GrantedAuthority> permissions = new ArrayList<>();List<String> permissionSnList =systemManageClient.listByUserId(loginFromMysql.getId());permissionSnList.forEach(e->{System.out.println("用户:"+username+" :加载权限 :"+e);permissions.add(new SimpleGrantedAuthority(e));});return new User(username,loginFromMysql.getPassword(),permissions);}
这里在通过UserDetailServer加载用户认证信息的时候就把用户的权限信息一并加载
- 登录测试
合理分配用户的权限,登录测试对于不同的资源是否应该有对应的访问权限
3.方法授权
SpringSecurity提供了一些授权的注解让我们可以在service,controller等的方法上贴注解进行授权,即在方法上指定方法方法需要什么样的权限才能访问
3.1.@Secured
标记方法需要有什么样的权限才能访问,这个注解需要在配置类上开启授权注解支持;@EnableGlobalMethodSecurity(securedEnabled=true) ,然后在Controller方法上贴该注解如:
@Secured(“IS_AUTHENTICATED_ANONYMOUSLY”) :方法可以匿名访问
@Secured(“ROLE_DEPT”) ,需要拥有部门的角色才能访问,ROLE_前缀是固定的
- 开启Secured授权支持
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- 使用@Secured进行方法授权
@RequestMapping("/employee/list")
@Secured("ROLE_employee:list")
public String list(){return "employee.list";
}
解释:这里使用了 @Secured(“ROLE_employee:list”) 意思是 “/employee/list” 这个资源需要“ROLE_employee:list”权限才能访问,如果认证的用户有该权限(UserDetailService中加载)包含了“ROLE_employee:list”即可访问该资源,否则不能访问。
注意:对于方法授权,没有贴注解的方法默认是匿名访问。@Secured注解授权是需要加上前缀“ROLE_”
3.2.@PreAuthorize
PreAuthorize适合进入方法前的权限验证,拥有和Secured同样的功能,甚至更强大,该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,然后在Controller贴注解如下:
@PreAuthorize(“isAnonymous()”) : 方法匿名访问
@PreAuthorize(“hasAnyAuthority(‘p_user_list’,‘p_dept_list’)”) :拥有p_user_listr或者p_dept_list的权限能访问
@PreAuthorize(“hasAuthority(‘p_transfer’) and hasAuthority(‘p_read_accout’)”) : 拥有p_transfer权限和p_read_accout权限才能访问.
该标签不需要有固定的前缀。
- 开启@PreAuthorize授权支持
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled= true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- 使用@PreAuthorize进行方法授权
@PreAuthorize("hasAnyAuthority('employee:add','employee:update')")
@RequestMapping("/employee/add")
public String add(){return "employee.add";
}
指明了方法必须要有 employee:add 或者 employee:update的权限才能访问 , 该注解不需要有固定的前缀。注意格式“@PreAuthorize(“hasAuthority(‘employee:add’)”)” ,hasAuthority不能省略,括号中是单引号。
3.3.@PostAuthorize
该注解使用并不多,适合在方法执行后再进行权限验证,使用该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,用法同 @PreAuthorize一样
五.认证结果处理-登峰造极
1.认证成功处理
1.1.解决方案
自定义类实现AuthenticationSuccessHandler接口复写 onAuthenticationSuccess方法,该方法其中一个参数是Authentication ,他里面封装了认证信息,用户信息UserDetails等,我们需要在这个方法中使用Response写出json数据即可
1.2.认证成功结果处理
- 定义AuthenticationSuccessHandler
定义类实现AuthenticationSuccessHandler接口复写onAuthenticationSuccess方法,实现自己的认证成功结果处理
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map map = new HashMap<>();map.put("success",true);map.put("message","认证成功");response.getWriter().print(JSON.toJSONString(map));response.getWriter().flush();response.getWriter().close();}
}
- 导入JSON依赖
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.50</version>
</dependency>
- 配置AuthenticationSuccessHandler
在SpringSecurity配置定义的AuthenticationSuccessHandler
http.formLogin()
//.successForwardUrl("/loginSuccess") // 设置登陆成功页
.successHandler(new MyAuthenticationSuccessHandler)
...
2.认证失败结果处理
2.1.解决方案
自定义登录失败的处理,需要实现AuthenticationFailureHandler接口,复写onAuthenticationFailure方法实现自己的认证失败结果处理
2.2.认证失败结果处理
- 定义处理器
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {Map map = new HashMap<>();map.put("success",false);map.put("message","认证失败");response.setStatus(HttpStatus.UNAUTHORIZED.value());response.getWriter().print(JSON.toJSONString(map));response.getWriter().flush();response.getWriter().close();}
}
- 配置处理器
http.formLogin()
.failureHandler(new MyAuthenticationFailureHandler)
...
六.授权结果处理-炉火纯青
1.授权失败处理
1.1.概述
当用户请求资源服务的资源时,需要进行用户的认证和授权检查,当认证或授权检查失败,我们需要要返回自己的失败结果信息,可以通过HttpSecurity设置授权失败结果处理器,内部通过 ExceptionTranslationFilter 调用AuthenticationEntryPoint实现匿名用户授权失败结果处理, ExceptionTranslationFilter 通过 AccessDeniedHandler来处理授权失败结果处理。
1.2.定义认证检查失败处理
- 定义AccessDeniedHandler
AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {String result = JSON.toJSONString(AjaxResult.me().setSuccess(false).setMessage("无访问权限"));response.setContentType("text/html;charset=utf-8");PrintWriter writer = response.getWriter();writer.print(result);writer.flush();writer.close();}
}
- 定义AuthenticationEntryPoint
AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {e.printStackTrace();httpServletResponse.setContentType("application/json;charset=utf-8");Map<String,Object> result = new HashMap<>();result.put("success",false);result.put("message","登录失败,用户名或密码错误["+e.getMessage()+"]");httpServletResponse.getWriter().print(JSONUtils.toJSONString(result));}
}
配置异常处理器
//异常处理
httpSecurity.exceptionHandling().accessDeniedHandler(new DefaultAccessDeniedHandler ())
.authenticationEntryPoint(new MyAuthenticationEntryPoint()) //身份认证验证失败配置
七.记住我-走火入魔
1.记住我概述
1.1.什么是记住我
Remember me(记住我)记住我,当用户发起登录勾选了记住我,在一定的时间内再次登录就不用输入用户名和密码了,即使浏览器退出重新打开也是如此。
1.2.流程分析
在SpringSecurity中提供RememberMeAuthenticationFilter过滤器来实现记住我功能,其核心流程如下:
- 认证成功UsernamePasswordAuthenticationFilter会调用RememberMeServices创建Token
(见其父类AbstractAuthenticationProcessingFilter.successfulAuthentication),同时 RemeberMeService 会调用TokenRepository将Token写入数据库(persistent_logins
),然后 RemeberMeService通 过Reponse.addCookie把Token写到浏览器的Cookies中 - 当浏览器再次发起请求会进入RemeberMeAuthenticationFilter,该Filter获取到请求cookies中的token交给RemeberMeService
- RemeberMeService调用TokenRepository去数据库中根据Token查询用户名
- 调用UserDetilasService.loadUserByUsername根据用户名获取用户认证信息
- 通过authenticationManager.authenticate,做一次认证,然后把用户信息放入上下文对象中SecurityContext
2.编码实战
2.1.修改该登录页
<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
</div>
增加记住我的选择框,注意name一定是”remember-me”
2.2.配置TokenRepository
PersistentTokenRepository是指明token的持久化方案(即用来存储“记住我”相关信息)。remember me功能是基于token,持久化方案有两种,一种基于内存,使用的是InMemoryTokenRepositoryImpl,一种基于数据库,使用的是JdbcTokenRepositoryImpl。这里我选择基于数据库的方式。
@Autowired
private DataSource dataSource ;@Bean
public PersistentTokenRepository persistentTokenRepository(){JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();obj.setDataSource(dataSource);obj.setCreateTableOnStartup(true); //启动创建表persistent_logs表,存token,username时会用到return obj;
}
配置持久化方案,用来存储“记住我”相关信息到数据库
如果:设置了“ obj.createTableOnStartUp(true);”之后会自动创建表persistent_logs,就不用再手动创建表。
2.3.配置rememberMe
修改认证服务配置WebSecurityConfig的 HttpSecurity配置,增加rememberMe
@Autowired
private UserDetailsService userDetailsService;
http.rememberMe().tokenRepository(persistentTokenRepository()) //持久.tokenValiditySeconds(3600) //过期时间.userDetailsService(userDetailsService); //用来加载用户认证信息的
2.4.创建persistent_logins 的表
注意,如果设置了 createTableOnStartUp(true); 就不用再执行该SQL
CREATE TABLE `persistent_logins` (`username` varchar(64) NOT NULL DEFAULT '',`series` varchar(64) NOT NULL,`token` varchar(64) NOT NULL,`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.5.测试
- 访问登录页面进行登录,勾选记住我
- 登录成功后,退出浏览器
- 重新打开浏览器,直接访问资源-不需要登录就能访问
扩展
自定义Filter,Provider实现自定义的认证流程,比如:短信验证码登录,三方登录
A092_SpringSecurity相关推荐
最新文章
- 人群分析--Beyond Counting: Comparisons of Density Maps for Crowd Analysis Tasks
- Microsoft office 2007 word PPT 转pdf的插件(转)
- 2021年春季学期-信号与系统-第六次作业参考答案-第九小题
- bnuoj 1065 简单的问题(位运算)
- python开发多平台app_django下创建多个app并设置urls方法
- Oracle 10g 数据库的备份和还原
- php7 ast,PHP7 的抽象语法树(AST)带来的变化
- 中断linux命令快捷键_实用!快速操作Linux终端命令行的快捷键
- 楼板计算塑形弹性_阶梯教室板模板支架工程方案计算书(仅供参考)
- 浅谈http协议六种请求方法,get、head、put、delete、post、options区别
- python数字信号处理应用pdf艾伦唐尼_Python数字信号处理应用
- 知乎2019新知青年大会:用问题改变世界的方向
- 前端播放二进制语音流
- 进公司不会用 Git 拉项目!第二天被开除?
- RuntimeWarning: Mean of empty slice
- iphone文件访问ftp服务器,ipad ftp服务器 iPhone/iPad访问FTP服务器设置步骤
- TSQL和PLSQL的区别
- python教程自带数据库_Python入门进阶教程-数据库操作
- C++中goto的使用
- 三维空间坐标系变换-旋转矩阵
热门文章
- 编程思维可以有效简化问题
- Swift3.0可选类型(Optional)倾心总结
- 如何使用PDF编辑软件旋转PDF页面
- DUF:Deep Video Super-Resolution Network Using Dynamic Upsampling Filters ...阅读笔记
- 机器学习的分类、回归、聚类问题
- 十个面向对象设计原则
- wordpress搬家主要流程分享本
- Android TextView字体加粗效果
- 苹果悬浮球_今天才发现!iPhone手机悬浮球这么好用!怪自己知道太晚了
- 面试题总结之windows/linux内存管理