目录
1、基本概念
1、认证
2、会话
3、授权
4、RBAC
1、基于角色的访问控制
2、基于资源的访问控制
2、基于Session的认证方式
3、Spring Security的使用(xml方式)
4、Spring Security的使用
5、Spring Security应用
1、SpringBoot集成
2、工作原理
3、认证和授权
1、认证流程
2、鉴权流程
3、自定义页面实现
4、会话
1、会话控制
2、会话超时
4、退出
5、授权
使用注解
用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
如何实现授权?业界通常基于RBAC实现授权。
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:
当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。
系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发给客户端的 sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在session 数据,以此完成用户的合法校验。当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于Session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法即可实现,如下是HttpSession相关的操作API。
org.springframework
spring-webmvc
5.1.5.RELEASE
javax.servlet
javax.servlet-api
3.0.1
provided
org.projectlombok
lombok
1.18.20
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
org.apache.maven.plugins
maven-compiler-plugin
1.8
本案例采用Servlet3.0无web.xml方式。
Spring容器配置
@Configuration //相当于applicationContext.xml
@ComponentScan(basePackages = "com.security"
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}
Spring Context配置
@Configuration //相当于springmvc.xml
@EnableWebMvc
@ComponentScan(basePackages = "com.security"
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
//视图解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/r*");
}
}
在init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。——加载Spring容器
//相当于web.xml
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,applicationContext.xml
@Override
protected Class>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}
//servletContext,springmvc.xml
@Override
protected Class>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/login",produces = {"text/plain;charset=UTF-8"})
public String login(UserRequest userRequest, HttpSession httpSession){
UserDto userDto = userService.login(userRequest);
httpSession.setAttribute("_user",userDto);
return userDto.getUsername()+"登录成功";
}
@RequestMapping(value = "/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(HttpSession session){
String result = "";
Object user = session.getAttribute("_user");
if(user == null){
result = "匿名访问";
}else{
UserDto userDto = (UserDto) user;
result = userDto.getFullname()+"欢迎访问----r1";
}
return result;
}
@RequestMapping(value = "/r2",produces = {"text/plain;charset=UTF-8"})
public String rr2(HttpSession session){
String result = "";
Object user = session.getAttribute("_user");
if(user == null){
result = "匿名访问";
}else{
UserDto userDto = (UserDto) user;
result = userDto.getFullname()+"欢迎访问----r2";
}
return result;
}
@RequestMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
}
@Service
public class UserServiceImpl implements UserService {
@Override
public UserDto login(UserRequest userRequest) {
if(userRequest == null || StringUtils.isEmpty(userRequest.getPassword())
|| StringUtils.isEmpty(userRequest.getUsername())){
throw new RuntimeException("账号或密码为空");
}
UserDto user = getUser(userRequest.getUsername());
if(user == null){
throw new RuntimeException("没有此用户");
}
if(!user.getPassword().equals(userRequest.getPassword())){
throw new RuntimeException("用户或密码错误");
}
return user;
}
private UserDto getUser(String username){
Map map = new HashMap<>();
Set a1 = new HashSet<>();
a1.add("r1");
Set a2 = new HashSet<>();
a2.add("r2");
map.put("aa",new UserDto(1,"aa","123","151525","张三",a1));
map.put("bb",new UserDto(2,"bb","321","369517","李四",a2));
return map.get(username);
}
}
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("_user");
response.setContentType("text/html;charset=utf-8");
if(user == null){
response.getWriter().println("请登录");
return false;
}
UserDto userDto = (UserDto) user;
String requestURL = request.getRequestURL().toString();
if(userDto.getAuthorities().contains("r1") && requestURL.contains("/r1")){
return true;
}
if(userDto.getAuthorities().contains("r2") && requestURL.contains("/r2")){
return true;
}
response.getWriter().println("你没有权限");
return false;
}
}
Spring Security基本使用_但盼风雨来 能留你在此的博客-CSDN博客
加上
org.springframework.security
spring-security-web
5.0.5.RELEASE
org.springframework.security
spring-security-config
5.0.5.RELEASE
spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
//内存方式定义用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("321").authorities("p1").build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
//无加密方式
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().
antMatchers("/r/r1").hasAnyAuthority("p1").//访问/r/r1必须具有p1权限
antMatchers("/r/r2").hasAnyAuthority("p2").
antMatchers("/r/*").authenticated().//所以/r*请求必须认证
anyRequest().permitAll().//其他请求直接通过
and().
formLogin().//允许表单登录
successForwardUrl("/login-success");//自定义登录成功页面地址
}
}
@Configuration //相当于springmvc.xml
@EnableWebMvc
@ComponentScan(basePackages = "com.security"
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视图解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//使用security自己的 login 页面
registry.addRedirectViewController("/","/login");
}
}
加载 WebSecurityConfig
//相当于web.xml
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,applicationContext.xml
@Override
protected Class>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
}
//servletContext,springmvc.xml
@Override
protected Class>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
Spring Security初始化,这里有两种情况:
在init包下定义SpringSecurityApplicationInitializer:
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
// public SpringSecurityApplicationInitializer() {
// super(WebSecurityConfig.class);
// }
}
@RestController
public class UserController {
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "登录成功";
}
@RequestMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
return "访问资源1";
}
@RequestMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
return "访问资源2";
}
}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
javax.servlet
javax.servlet-api
provided
javax.servlet
jstl
org.springframework.boot
spring-boot-starter-tomcat
provided
org.apache.tomcat.embed
tomcat-embed-jasper
org.projectlombok
lombok
1.18.20
org.springframework.boot
spring-boot-starter-test
test
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
org.apache.maven.plugins
maven-compiler-plugin
1.8
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
server.port=8080
server.servlet.context-path=/springboot-security
spring.application.name=springboot-security
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//使用security自己的 login 页面
registry.addRedirectViewController("/","/login");
}
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
//内存方式定义用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("321").authorities("p1").build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
//无加密方式
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().
antMatchers("/r/r1").hasAnyAuthority("p1").//访问/r/r1必须具有p1权限
antMatchers("/r/r2").hasAnyAuthority("p2").
antMatchers("/r/*").authenticated().//所以/r*请求必须认证
anyRequest().permitAll().//其他请求直接通过
and().
formLogin().//允许表单登录
successForwardUrl("/login-success");//自定义登录成功页面地址
}
}
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,SpringSecurity对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
FilterChainProxy 是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器
(AccessDecisionManager)进行处理,上图是FilterChainProxy相关类的UML图示。spring Security功能的实现主要是由一系列过滤器链相互配合完成。
让我们仔细分析认证过程:
Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。SpringSecurity使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
decide的参数:
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//使用security自己的 login 页面
registry.addViewController("/").setViewName("redirect:/login-view");
registry.addViewController("/login-view").setViewName("login");
}
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
//内存方式定义用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("321").authorities("p1").build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
//无加密方式
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().//屏蔽CSRF,使security 不再限制CSRF
authorizeRequests().
antMatchers("/r/r1").hasAnyAuthority("p1").//访问/r/r1必须具有p1权限
antMatchers("/r/r2").hasAnyAuthority("p2").
antMatchers("/r/*").authenticated().//所以/r*请求必须认证
anyRequest().permitAll().//其他请求直接通过
and().
formLogin().//允许表单登录
loginPage("/login-view").//登录页面
loginProcessingUrl("/login").
successForwardUrl("/login-success").//自定义登录成功页面地址
permitAll();
}
}
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
从数据库中获取用户
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.getUser(username);
if(user == null){
//如果查不到,返回null,由provider抛异常
return null;
}
return org.springframework.security.core.userdetails.User.
withUsername(user.getUsername()).
password(user.getPassword()).
authorities("p1").build();
}
}
获取登录用户信息
@RequestMapping(value = "/get",produces = {"text/plain;charset=UTF-8"})
public String getUserName(){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal == null){
return "匿名";
}
if(principal instanceof UserDetails){
UserDetails user = (UserDetails) principal;
return "你好,"+user.getUsername();
}else{
return principal.toString();
}
}
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是 ifRequired 。
若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了session,那么Spring Security会用它的。
若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制。
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s;
server.servlet.session.timeout=3600s
session超时之后,可以通过Spring Security 设置跳转的路径。
http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
expired指session过期,invalidSession指传入的sessionid无效。
我们可以使用httpOnly和secure标签来保护我们的会话cookie:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
当退出操作出发时,将发生:
注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用post方式请求/logout。
logoutHandler:
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现:
链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如deleteCookies()。
授权的方式包括 web授权和方法授权,web授权是通过 url拦截进行授权,方法授权是通过 方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor;若为方法授权则拦截器为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.getUser(username);
if(user == null){
//如果查不到,返回null,由provider抛异常
return null;
}
List list = userDao.getPermission(user.getId());
String[] arr = new String[list.size()];
list.toArray(arr);
return org.springframework.security.core.userdetails.User.
withUsername(user.getUsername()).
password(user.getPassword()).
authorities(arr).build();
}
注意:
规则的顺序是重要的,更具体的规则应该先写.现在以/ admin开始的所有内容都需要具有ADMIN角色的身份验证用户,即使是/ admin / login路径(因为/ admin / login已经被/ admin / **规则匹配,因此第二个规则被忽略).
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/admin/login").permitAll()
因此,登录页面的规则应该在/ admin / **规则之前.例如.
.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
保护URL常用的方法有:
从Spring Security2.0版本开始,它支持服务层方法的安全性的支持。
我们可以在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注释来启用基于注解的安全性。
@PreAuthorize,@PostAuthorize, @Secured三类注解。
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
以上配置标明readAccount、findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器。
ublic interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public Account post(Account account, double amount);
}
以上配置标明readAccount、findAccounts方法可匿名访问,post方法需要同时拥有p_transfer和p_read_account权限才能访问,底层使用WebExpressionVoter投票器,