用户授权
基于角色或权限进行访问
1、配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问所跳转的自定义403页面
http.exceptionHandling().accessDeniedPage("/403.html");
//开启自定义登陆页面
http.formLogin()
//设置登陆页面
.loginPage("/index.html")
//登录访问路径
.loginProcessingUrl("/user/login")
//登录成功后,所跳转路径
.defaultSuccessUrl("/test/success").permitAll()
.and()
//设置不需要认证,可以直接访问的页面
.authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll()
//所有用户都可以访问
.anyRequest().authenticated();
}
}
2、hasAuthority
/*
指定URL需要一个特定的授权
形参:
authority - 需要的权限(即 ROLE_USER, ROLE_ADMIN, 等等)
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}
private static String hasAuthority(String authority) {
return "hasAuthority('" + authority + "')";
}
(1)针对允许某一权限的操作
(2)判断当前主体是否具有指定权限,有返回 true,没有返回 false
(3)在配置类设置,当前访问地址所需权限
//当前登录用户,只有具有admin权限才可以访问指定路径
http.antMatchers("/test/index").hasAuthority("admin")
(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面
3、hasAnyAuthority
/*
指定URL需要输入权限中的任何一个
形参:
authority - 请求至少需要一个授权(例如,"ROLE_USER", "ROLE_ADMIN "意味着需要 "ROLE_USER "或 "ROLE_ADMIN")
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) {
return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));
}
private static String hasAnyAuthority(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
return "hasAnyAuthority('" + anyAuthorities + "')";
}
(1)针对允许多个权限的操作
(2)如果当前主体拥有任一权限,返回 true,否则返回 false
(3)在配置类设置,当前访问地址所需权限
//当前登录用户,只有具有admin权限才可以访问指定路径,参数为多个权限字符串,或使用,分隔包含多个权限的一个字符串
http.antMatchers("/test/index").hasAnyAuthority("admin","manager")
(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面
4、hasRole
/*
用于指定需要特定角色的URL的快捷方式。如果你不想让角色前缀(默认为 "ROLLE_")自动插入,请参阅hasAuthority(String)
形参:
role - 需要的角色(例如:USER、ADMIN等)。注意,不应该以ROLE_开始,因为这将被自动插入
返回值:
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasRole(String role) {
return access(ExpressionUrlAuthorizationConfigurer
.hasRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, role));
}
private static String hasRole(String rolePrefix, String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> "role should not start with '"
+ rolePrefix + "' since it is automatically inserted. Got '" + role + "'");
return "hasRole('" + rolePrefix + role + "')";
}
(1)针对允许某一权限的操作
(2)如果当前主体具有指定的角色,则返回 true,否则返回 false
(3)在配置类设置,当前访问地址所需权限
//添加默认前缀ROLE_,拼接为ROLE_admin
http.antMatchers("/test/index").hasAuthority("admin")
(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面
5、hasAnyRole
/*
指定URL需要许多角色中的任何一个的快捷方式。如果你不想让角色前缀(默认为 "ROLLE_")自动插入,请参阅hasAnyAuthority(String...)
形参:
roles - 需要的角色(例如:USER、ADMIN等)。注意,它不应该以ROLE_开始,因为它会被自动插入
返回值;
ExpressionUrlAuthorizationConfigurer 用于进一步定制
*/
public ExpressionInterceptUrlRegistry hasAnyRole(String... roles) {
return access(ExpressionUrlAuthorizationConfigurer
.hasAnyRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, roles));
}
private static String hasAnyRole(String rolePrefix, String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
}
(1)针对允许多个权限的操作
(2)如果当前主体拥有任一权限,返回 true,否则返回 false
(3)在配置类设置,当前访问地址所需权限
//添加默认前缀ROLE_,拼接所有角色,参数为多个权限字符串,或使用,分隔包含多个权限的一个字符串
http.antMatchers("/test/index").hasAnyAuthority("admin","manager")
(4)在 UserDetailsService 接口实现类的 loadUserByUsername 方法中,设置 User 对象权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
(5)当前用户拥有访问权限,则访问页面;没有访问权限,显示 403 页面
使用注解认证授权
1、需要开启注解支持,注解在启动类上
@EnableGlobalMethodSecurity(securedEnabled=true)
2、@Secured
(1)设置哪些角色可以访问方法,需要添加前缀,默认为 ROLE_
(2)示例
@RequestMapping("/testSecured")
@ResponseBody
@Secured({"ROLE_admin","ROLE_user"})
public String testSecured(){
return "testSecured";
}
3、@PreAuthorize
(1)进入方法前的权限验证
(2)示例
@RequestMapping("/testPreAuthorize")
@ResponseBody
//可使用hasRole、hasAnyRole、hasAuthority、hasAnyAuthority
@PreAuthorize("hasAuthority('admin')")
public String testPreAuthorize(){
return "testPreAuthorize";
}
4、@PostAuthorize
(1)在方法执行后,再进行权限验证,适合验证带有返回值的权限,较少使用
(2)示例
@RequestMapping("testPostAuthorize")
@ResponseBody
//可使用hasRole、hasAnyRole、hasAuthority、hasAnyAuthority
@PostAuthorize("hasAuthority('admin')")
public String testPostAuthorize(){
return "testPostAuthorize";
}
5、@PostFilter
(1)权限验证之后,对返回数据进行过滤,留下符合条件的数据,较少使用
(2)示例
@RequestMapping("testPostFilter")
@ResponseBody
//表示只保留username == 'admin'的数据
@PostFilter("filterObject.username == 'admin'")
public List<UserInfo> testPostFilter(){
List<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1,"admin","1234"));
list.add(new UserInfo(2,"user","1234"));
return list;
}
6、@PreFilter
(1)进入控制器之前,对传入数据进行过滤,较少使用
(2)示例
@RequestMapping("testPreFilter")
@ResponseBody
//表示只接收username == 'user'的数据
@PostFilter("filterObject.username == 'user'")
public List<UserInfo> testPreFilter(){
List<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1,"admin","1234"));
list.add(new UserInfo(2,"user","1234"));
return list;
}
用户注销
1、登录页面添加退出链接
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功
<br>
<a href="/logout">退出</a>
</body>
</html>
2、在配置类中添加退出映射地址
http.logout()
//设置退出路径
.logoutUrl("/logout")
//退出成功后,所跳转的路径
.logoutSuccessUrl("/index")
//允许所有用户
.permitAll();
自动登录
1、原理
(1)第一次登陆:客户端存放 Cookie 加密串,数据库存放加密串、用户信息字符串
(2)再次登录:获取 Cookie 信息,与数据库的加密串比对,认证成功,自动登录
2、Spring Security 底层
(1)第一次认证请求:UsernamePasswordAuthenticationFilter
(2)认证成功:RememberMeServices 使用 TokenReposity 生成 Token,将 Token 写入浏览器 Cookie,将 Token 写入数据库
(3)再次请求:RememberMeAuthenticationFilter
(4)RememberMeServices 读取 Cookie 中的 Token,UserDetailsService 查找数据库中的 Token,比对两者
3、实现示例
(1)表结构
CREATE TABLE `persistent logins`
(
`username` VARCHAR(64) NOT NULL,
`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)配置类:注入数据源;配置操作数据库对象 JdbcTokenRepositoryImpl;配置自动登录
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置操作数据库对象:JdbcTokenRepositoryImpl,为PersistentTokenRepository的实现类
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//设置数据源
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问所跳转的自定义403页面
http.exceptionHandling().accessDeniedPage("/403.html");
//开启自定义登陆页面
http.formLogin()
//设置登陆页面
.loginPage("/index.html")
//登录访问路径
.loginProcessingUrl("/user/login")
//登录成功后,所跳转路径
.defaultSuccessUrl("/test/success").permitAll()
.and()
//设置不需要认证,可以直接访问的页面
.authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll()
//所有用户都可以访问
.anyRequest().authenticated()
//指定已经记住的用户允许使用的URL
.and().rememberMe()
//指定要使用的PersistentTokenRepository,默认使用TokenBasedRememberMeServices
.tokenRepository(persistentTokenRepository())
//设置自动登录的有效时长,单位为秒
.tokenValiditySeconds(60)
//设置userDetailsService操作数据库
.userDetailsService(userDetailsService);
}
}
(3) 登录页面添加复选框 remember-me,该复选框的 name 属性固定名称
记住密码<input type="checkbox" name="remeber-me" title="下次自动登录"/><br/>
CSRF
1、Cross-site request forgery:跨站请求伪造
2、一种挟制用户在当前已登录的 Web 应用程序上,执行非本意的操作的攻击方法:攻击者通过一些技术手段欺骗用户的浏览器,去访问一个自己曾经认证过的网站并运行一些操作,由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行
3、与跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任
4、利用 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的
5、从 Spring Security 4.0 开始,默认启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 针对 PATCH、POST、PUT、DELETE 方法进行防护
6、页面表单添加隐藏域
<!--
使用Thymeleaf语法
传入参数名、Token值
-->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
7、Spring Security 实现 CSRF 原理
(1)第一次请求:CsrfTokenRepository 生成 CsrfToken,保存到 HttpSession、Cookie 中
(2)再次请求:Cookie 携带 CsefToken 字符串,与 Session 储存的 Token 比对,CsrfFilter 判断是否合法
8、源码
(1)CsrfToken 接口
public interface CsrfToken extends Serializable {
//获取CSRF在响应中填入的HTTP头名称,并可以放在请求中,而不是参数中,不能为空
String getHeaderName();
//获取应包含Token的HTTP参数名称,不能为空
String getParameterName();
//获取令牌值,不能为空
String getToken();
}
(2)CsrfToken 实现类:SaveOnAccessCsrfToken(LazyCsrfTokenRepository 的静态内部类)、DefaultCsrfToken(默认)
(3)SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 CsrfToken 时,才会去对 CsrfToken 做保存操作,调用 HttpSessionCsrfTokenRepository 或 CookieCsrfTokenRepository 的 saveToken 方法
public String getToken() {
saveTokenIfNecessary();
return this.delegate.getToken();
}
private void saveTokenIfNecessary() {
if (this.tokenRepository == null) {
return;
}
synchronized (this) {
if (this.tokenRepository != null) {
this.tokenRepository.saveToken(this.delegate, this.request, this.response);
this.tokenRepository = null;
this.request = null;
this.response = null;
}
}
}
(4)CsrfTokenRepository 接口
public interface CsrfTokenRepository {
/*
生成一个CsrfToken
形参:
request - 要使用的HttpServletRequest
返回值:
被生成的CsrfToken,不能为空
*/
CsrfToken generateToken(HttpServletRequest request);
/*
使用HttpServletRequest和HttpServletResponse保存CsrfToken,如果CsrfToken为空,则表示删除它
形参:
token - 要保存的CsrfToken或删除的空值
request - 使用的HttpServletRequest
response - 要使用的HttpServletResponse
*/
void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
/*
从HttpServletRequest中加载预期的CsrfToken
形参:
request - 要使用的HttpServletRequest
返回值:
CsrfToken,如果不存在则为空
*/
CsrfToken loadToken(HttpServletRequest request);
}
(5)CsrfTokenRepository 实现类:CookieCsrfTokenRepository、HttpSessionCsrfTokenRepository + LazyCsrfTokenRepository(默认)
(6)LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,而是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或 CookieCsrfTokenRepository 的功能
/*
生成一个新的令牌
形参:
request - 要使用的HttpServletRequest,HttpServletRequest必须有HttpServletResponse作为属性,名称为HttpServletResponse.class.getName()
*/
public CsrfToken generateToken(HttpServletRequest request) {
return wrap(request, this.delegate.generateToken(request));
}
/*
如果CsrfToken不是空的,什么都不做;如果它是空的,那么就立即执行保存
只有当CsrfToken.getToken()从generateToken(HttpServletRequest)中被访问时才会进行保存
*/
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
(7)CsrfFilter 类
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//从CsrfTokenRepository中获取CsrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
//如果找不到CsrfToken,说明该请求是第一次发起
if (missingToken) {
//生成一个CsrfToken
csrfToken = this.tokenRepository.generateToken(request);
//保存到CsrfTokenRepository
this.tokenRepository.saveToken(csrfToken, request, response);
}
//在请求中添加CsrfToken,默认情况下,通过JSP或Thymeleaf标签渲染_csrf的数据来源
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//判断哪些请求方法需要做校验
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
//先从请求头中获取CsrfToken
String actualToken = request.getHeader(csrfToken.getHeaderName());
//请求头无法获取CsrfToken,再从请求参数中获取
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
//如果请求所携带的CsrfToken与从Repository中获取的不同,则抛出异常
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
//正常情况下继续执行过滤器链的后续流程
filterChain.doFilter(request, response);
}
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
//表示GET、HEAD、TRACE、OPTIONS请求不受保护
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
@Override
public String toString() {
return "CsrfNotRequired " + this.allowedMethods;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战