Spring Security的使用
spring security
使用目的:验证,授权,攻击防护。
原理:创建大量的filter和interceptor来进行请求的验证和拦截,以此来达到安全的效果。
Spring Security主要包括两大功能:验证和鉴权。验证就是确认用户的身份,一般采用用户名和密码的形式;鉴权就是确认用户拥有的身份(角色、权限)能否访问受保护的资源。
鉴权则是一系列判断用户是否有权限访问资源的过程。
1.当用户未登录时,访问任何需要权限的资源都会转向登录页面,尝试进行登录;
2.当用户登录成功时,他会获得一系列角色。
3.用户访问某资源/xxx时,FilterInvocationSecurityMetadataSource这个类的实现类(本文是MySecurityMetadataSource)会调用getAttributes方法来进行资源匹配。它会读取数据库resource表中的所有记录,对/xxx进行匹配。若匹配成功,则将/xxx对应所需的角色组成一个 Collection<ConfigAttribute>返回;匹配不成功则说明/xxx不需要什么额外的访问权限;
4.流程来到鉴权的决策类AccessDecisionManager的实现类(MyAccessDecisionManager)中,它的decide方法可以决定当前用户是否能够访问资源。decide方法的参数中可以获得当前用户的验证信息、第3步中获得的资源所需角色信息,对这些角色信息进行匹配即可决定鉴权是否通过。当然,你也可以加入自己独特的判断方法,例如只要用户具有ROLE_ADMIN角色就一律放行;
5.若鉴权成功则用户顺利访问页面,否则在decide方法中抛出AccessDeniedException异常,这个异常会被AccessDeniedHandler的实现类(MyAccessDeniedHandler)处理。它仅仅是生成了一个json对象,转换为字符串返回给客户端了。
新建一个springboot项目
创建一个springboot项目,添加一个
/hello
Controller
这样,这个/hello
是可以默认访问,返回一个hello字符串。
添加spring security
向pom.xml中添加security依赖
这样在启动的时候会在控制台显示随机生成的密码。
这时访问http://localhost:8080/hello会重定向到http://localhost:8080/login,这个页面是spring默认的。
登录
使用默认用户和随机生成的密码登录
spring security 默认的用户名是user,spring security启动的时候会生成默认密码(在启动日志中可以看到)。
我们填入user 和 上图显示的fa028beb-31f0-4ccd-be91-31ba4a0cdb8d,那么就会正常的访问/hello
。
使用yaml文件定义的用户名、密码登录
在application.yml中定义用户名密码:
使用root/root登录,可以正常访问/hello
。
使用代码中指定的用户名、密码登录
- 使用configure(AuthenticationManagerBuilder) 添加认证。
- 使用configure(httpSecurity) 添加权限
添加AdminController、ProductController
通过上面的设置,访问http://localhost:8080/admin/hello只能由admin访问,http://localhost:8080/product/hello admin和user都可以访问,http://localhost:8080/hello 所有用户(包括tmp)都可以访问。
使用数据库的用户名、密码登录
添加依赖
添加数据库配置
配置spring-security认证和授权
如果需要使用BCryptPasswordEncoder
,可以先在测试环境中加密后放到数据库中:
配置自定义UserDetailsService来进行验证
配置JPA中的UserRepository
添加数据库数据
默认角色前缀必须是
ROLE_
,因为spring-security会在授权的时候自动使用match中的角色加上ROLE_
后进行比较。
获取登录信息
使用SecurityContextHolder.getContext().getAuthentication().getPrincipal();
获取当前的登录信息。
Spring Security 核心组件
SecurityContext
SecurityContext
是安全的上下文,所有的数据都是保存到SecurityContext中。
可以通过SecurityContext
获取的对象有:
- Authentication
SecurityContextHolder
SecurityContextHolder
用来获取SecurityContext中保存的数据的工具。通过使用静态方法获取SecurityContext的相对应的数据。
Authentication
Authentication表示当前的认证情况,可以获取的对象有:
UserDetails:获取用户信息,是否锁定等额外信息。
Credentials:获取密码。
isAuthenticated:获取是否已经认证过。
Principal:获取用户,如果没有认证,那么就是用户名,如果认证了,返回UserDetails。
UserDetails
UserDetailsService
UserDetailsService可以通过loadUserByUsername获取UserDetails对象。该接口供spring security进行用户验证。
通常使用自定义一个CustomUserDetailsService来实现UserDetailsService接口,通过自定义查询UserDetails。
AuthenticationManager
AuthenticationManager用来进行验证,如果验证失败会抛出相对应的异常。
PasswordEncoder
密码加密器。通常是自定义指定。
BCryptPasswordEncoder:哈希算法加密
NoOpPasswordEncoder:不使用加密
spring security session 无状态支持权限控制(前后分离)
spring security会在默认的情况下将认证信息放到HttpSession中。
但是对于我们的前后端分离的情况,如app,小程序,web前后分离等,httpSession就没有用武之地了。这时我们可以通过
configure(httpSecurity)
设置spring security是否使用httpSession。
共有四种值,其中默认的是ifRequired。
- always – a session will always be created if one doesn’t already exist,没有session就创建。
- ifRequired – a session will be created only if required (default),如果需要就创建(默认)。
- never – the framework will never create a session itself but it will use one if it already exists
- stateless – no session will be created or used by Spring Security 不创建不使用session
由于前后端不通过保存session和cookie来进行判断,所以为了保证spring security能够记录登录状态,所以需要传递一个值,让这个值能够自我验证来源,同时能够得到数据信息。选型我们选择JWT。对于java客户端我们选择使用jjwt。
添加依赖
创建工具类JWTProvider
JWTProvider需要至少提供两个方法,一个用来创建我们的token,另一个根据token获取Authentication。
provider需要保证Key密钥是唯一的,使用init()构建,否则会抛出异常。
注意这里我们创建的User需要实现UserDetails对象,这样我们可以根据
principal.getAuthorities()
获取到权限,如果不实现UserDetails,那么需要自定义authorities并添加到UsernamePasswordAuthenticationToken中。
创建登录成功,登出成功处理器
登录成功后向前台发送jwt。
认证成功,返回jwt:
登出成功:
设置登录、登出、取消csrf防护
登出无法对token进行失效操作,可以使用数据库保存token,然后在登出时删除该token。
使用JWT集成spring-security
添加Filter供spring-security解析token,并向securityContext中添加我们的用户信息。
在UsernamePasswordAuthenticationFilter.class之前我们需要执行根据token添加authentication。关键方法是从jwt中获取authentication,然后添加到securityContext中。
在SecurityConfiguration中需要设置Filter添加的位置。
创建自定义Filter,用于jwt获取authentication:
向HttpSecurity添加Filter和设置Filter位置:
MySecurityConfiguration代码
使用注解对方法进行权限管理
需要在
MySecurityConfiguration
上添加@EnableGlobalMethodSecurity(prePostEnabled = true)
注解,prePostEnabled默认为false,需要设置为true后才能全局的注解权限控制。
prePostEnabled设置为true后,可以使用四个注解:
添加实体类School:
-
@PreAuthorize
在访问之前就进行权限判断
@RestController public class AnnoController { @Autowired private JWTProvider jwtProvider; @RequestMapping("/annotation") // @PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public String info(){ return "拥有admin权限"; } }
-
hasRole和hasAuthority都会对UserDetails中的getAuthorities进行判断区别是hasRole会对字段加上
ROLE_
后再进行判断,上例中使用了hasRole('ADMIN')
,那么就会使用ROLE_ADMIN
进行判断,如果是hasAuthority('ADMIN')
,那么就使用ADMIN
进行判断。 -
@PostAuthorize
在请求之后进行判断,如果返回值不满足条件,会抛出异常,但是方法本身是已经执行过了的。
returnObject是内置对象,引用的是方法的返回值。
如果
returnObject.id%2==0
为 true,那么返回方法值。如果为false,会返回403 Forbidden。 -
@PreFilter
在方法执行之前,用于过滤集合中的值。
filterObject
是内置对象,引用的是集合中的泛型类,如果有多个集合,需要指定filterTarget
。filterObject%2==0
会对集合中的值会进行过滤,为true的值会保留。第一个例子返回的值在执行前过滤返回2,4。
-
@PostFilter
会对返回的集合进行过滤。
上面的方法返回结果为:id为0,2,4,6,8的School对象。