Spring Security - 我爱学习网 (5axxw.com)
底层是filter,经过一系列filter,对请求进行处理,最终到达servlet。 共有11个过滤器。
组件
SecurityContext :上下文对象,存储认证后的Authenation 。
SecurityContextHolder :用于获取SecurityContext 的工具类。
AuthenticationManager :用于校验Authentication,返回一个认证完成后的Authentication对象,默认实现类是ProviderManager。 Authentication : 用户的认证信息,有三个核心属性:
- principal: 用户的身份信息
- credentails: 用户认证凭据,比如密码。认证完成后,此项会被清空。
- authorities:用户权限。
Authentication的两个主要作用:
- 为 AuthenticationManager 对象提供用于认证的信息载体;
- 用于获取某个用户的基本信息。
在 Spring Boot 方式下启动 Spring Security 工程,将会自动开启如下配置项:
-
默认开启一系列基于 springSecurityFilterChain 的 Servlet 过滤器,包含了几乎所有的安全功能,例如:保护系统 URL、验证用户名、密码表单、重定向到登录界面等;
-
创建 UserDetailsService 实例,并生成随机密码,用于获取登录用户的信息详情;
-
将安全过滤器应用到每一个请求上。
配置类
继承WebSecurityConfigurerAdapter, 加注解 定义类AuthenticationManager
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// 除上面外的所有请求全部需要认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 验证token的拦截器放在UsernamePasswordAuthenticationFilter之前
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
// httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
// httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 对密码进行加密 非明文加密 用户输入的密码传来后被security加密,之后判断用的是加密的密码
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
// 密码加密
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
自定义UserDetail实现类
public class LoginUser implements UserDetails {
private Long userId;
private Long deptId;
// 用户唯一标识
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
// 权限列表
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
自定义UserDetailsService实现类
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}",s);
// 根据用户名验证用户
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
UserInfo userInfo = userService.getOne(queryWrapper);
if (userInfo == null) {
throw new UsernameNotFoundException("用户名不存在,登陆失败。");
}
// 构建UserDetail对象
UserDetail userDetail = new UserDetail();
userDetail.setUserInfo(userInfo);
List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
userDetail.setRoleInfoList(roleInfoList);
return userDetail;
}
}
认证过程中SpringSecurity会调用这个方法查询出用户,验证登录用户信息是否正确。将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。
认证过滤器
自定义未授权处理
自定义退出处理
注解
@EnableWebSecurity作用 组合注解。
@Import({EnableGlobalMethodSecurity})激活了websecurityConfiguration配置类。 注入了一个非常重要的bean,bean的name为springSecurityFilterChain,这是springsecurity的过滤器,是请求的认证入口。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
@EnableGlobalAuthentication
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({GlobalMethodSecuritySelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
boolean prePostEnabled() default false; 开启后支持springEL表达式。
boolean securedEnabled() default false;
boolean jsr250Enabled() default false;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
这个注解中引入了人AuthenticationConfiguration配置类,这个类用于配置认证相关的类, 向spring容器中注入了AuthenticationManagerBuilder,AuthenticationManagerBuilder使用了建造者模式。 所以@EnableWebSecurity有两个作用: 1.加载websecurityConfigurstion配置类,配置安全认证策略。 2.加载了AuthenticationConfigurstion配置了认证信息。
过滤器是如何加入到过滤器链中的
以FilterComparator提供的规则进行比较,按照比较结果进行排序注册。 通过HttpSecurity的addFilter方法加入到过滤器链中。
@PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
在方法执行之前就起作用。如果满足条件就能执行方法。
支持自定义的权限判断。 ss.hasPermi() 就是自定义了一个类,并且由自定义的判断权限的方法,返回true或false
认证
表单认证
表单认证是最常用的一个认证方式,一个最直观的业务场景便是用账号密码登录,UsernamePasswordAuthenticationFilter,在整个认证体系中至关重要。 登陆后进入UsernamePasswordAuthenticationFilter,进入attemptAuthentication方法中,拿到用户名和密码,
鉴权
http无状态,所以每次访问都是一个新的请求,那么服务器就只能根据token来判断你是谁。 每次请求都会被JwtAuthenticationTokenFilter拦截,取出请求头中的token。 根据token获取用户信息,然后创建一个UsernamePasswordAuthenticationToken,把它存到security的上下文,这个就是授权用户,否则就是匿名用户。 后续的过滤器就能让请求通过。否则请求会被拦住。
用户的权限信息会存到authentication。 后续请求会从authentication中获取权限进行判断。
运行中,会调用hasAuthority()方法进行校验. hasAuthority可以自己重写.
springsecurity还为我们提供了其他方法,hasanyauthoritity,hasrole,hasanyrole.
hasanyauthoritity可以指定多个权限.具有其中一个,就可以访问
hasAuthority指定一个权限
hasrole可以访问的角色
hasanyrole多个可以访问的角色。
过滤器执行过程
SecurityContextHolderAwareRequestFilter
FilterSecurityInterceptor 权限验证,验证不通过会抛出异常; ExceptionTransLationFilter处理抛出的异常 。 AnonymousAuthenticationFilter 如果security上下文中没有认证信息Authentication,就给上下文添加一个匿名用户,防止后面过滤器运行时发生空指针异常。
配置动态权限:
RBAC
用户角色权限模型。 根据模型的复杂程度,可分为RBAC0,RBAC1,RBAC2,RBAC3
RBAC0:用户角色权限之间是多对多 RBAC1:角色分成了多个等级
用户组:直接给用户组(部门)分配角色,再把用户加入用户组,这样用户不仅有自己的角色,还有所属用户组的角色。
权限字符串
system:user:list 自定义。 模块:表:操作
jwt
服务器如何记住用户?靠的是会话跟踪。常用的会话跟踪技术是cookie,session,token。 cookie通过客户端记录信息,session通过服务端记录信息。 web程序是基于HTTP协议传输数据的,而HTTP协议是无状态的,每次请求完后都会断开连接,再次请求需要重新建立连接。这就意味着服务器无法根据连接来跟踪会话。 那就需要特殊的机制。 无状态请求:服务端处理的信息是服务端保存的 有状态请求:服务端处理的信息来自于请求所携带的 有状态的服务常用于实现事务,如添加购物车,付款时从购物车获取商品信息。 无状态服务,为了伸缩性考虑,实现服务端水平扩展。请求就可以发到任意一个服务器。
同源策略: 同源指两个页面具有相同的协议,主机和端口号。 发生跨域时,cookie是不起作用的。
跨域: CORS 普通跨域请求:只需服务端设置。
带cookie的跨域:前后端都进行设置。 服务端在response中配置了,浏览器检测到,就能允许ajax进行跨域访问。
-
服务端设置:response.setHeader("Access-Control-Allow- origin","*"); 允许跨域访问的域名,*表示任何请求都可以跨域
-
response.setHeader("Access-Control-Allow-Credentials","true"); 允许前端带认证cookie,启用此项后,上面的域名不能为*
-
response.seteader("Access-control-Allow-Headers","content-type,X-Requested-with");
cookie的常见场景: 记录用户的一些信息
识别session,需要客户端携带的cookie 这个cookie是自动生成的,仅当前浏览器内有效。 session在分布式下不好。
服务端不保存用户信息, 在token中保存一些用户相关的信息,发给客户端。 客户端请求时携带token字符串,服务端解析token,去查询用户(可以用数据库也可以用redis。最好redis),没查到用户信息说明未登录。
token由三部分数据组成: header:。 payload:通常会保存一些与用户相关的信息,如用户名;存储过期时间。但是不能放太多信息。 signature:header+payload+secret
secret:令牌秘钥 生成签名的时候使用,secret是服务端的,不能暴露出去。
token存在哪里? cookie不建议(CSRF攻击) http请求头。 CSRF攻击:攻击者伪造请求,并获取cookie,这样攻击者的请求就能通过验证。 解决方法是在请求中放入不能伪造的信息,并且不能存在cookie中,因此可以使用token,在后端拦截请求验证token。
CSRF攻击过程: 用户访问正规网站A,网站A生成cookie,并保存在用户浏览器中。 攻击者在网站A发布一些信息并加上网站B的链接,诱导用户访问网站B. 可以认为是钓鱼网站。 用户访问了网站B,B网站就获取了用户发过来的cookie 网站B携带着用户的cookie去访问网站A,网站A就会响应 防护措施:
TokenUtil
采用JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:
创建token 验证token 反解析token中的信息 做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放在UsernamePasswordAuthenticationFilter前面。
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain) throws ServletException, IOException {
log.info("JWT过滤器通过校验请求头token进行自动登录...");
// 拿到Authorization请求头内的信息
String authToken = jwtProvider.getToken(request);
// 判断一下内容是否为空且是否为(Bearer )开头
if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
// 去掉token前缀(Bearer ),拿到真实token
authToken = authToken.substring(jwtProperties.getTokenPrefix().length());
// 拿到token里面的登录账号
String loginAccount = jwtProvider.getSubjectFromToken(authToken);
if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 缓存里查询用户,不存在需要重新登陆。
UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);
// 拿到用户信息后验证用户信息与token
if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {
// 组装authentication对象,构造参数是Principal Credentials 与 Authorities
// 后面的拦截器里面会用到 grantedAuthorities 方法
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
}
}
}
chain.doFilter(request, response);
}
从请求中取出token->解析token->token认证->security认证 登录->拿到token->请求带上token->JWT过滤器拦截->校验token->从缓存里面拿我们的UserDetail-> 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了
Oauth2
一个关于授权的开放网络标准。
不使用本系统的账号密码,同意第三方应用进入系统,系统会生成一个短期的token,供第三方使用。 一些网站允许使用其他方式登录,qq,微信等,就是第三方登录。 token和密码 token是临时的,到期失效 token可以被所有者撤销。 token有权限范围。