作者:清茶淡粥酱
链接:https://juejin.cn/post/7026734817853210661
Spring Security 是一种高度自定义的安全框架,利用(基于)SpringIOC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
核心功能:认证和授权

Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Boot 基础就不介绍了,推荐下这个实战教程:
导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
在浏览器输入:http://localhost:8080/ 进入Spring Security内置登录页面
用户名: user
密码:项目启动,打印在控制台中
修改application.yml 文件
# 静态用户,一般只在内部网络认证中使用,如:内部服务器1,访问服务器2
spring:
security:
user:
name: test # 通过配置文件,设置静态用户名
password: test # 配置文件,设置静态登录密码
什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService接口
@Component
public class UserSecurity implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userService.login(userName);
System.out.println(user);
if (null==user){
throw new UsernameNotFoundException("用户名错误");
}
org.springframework.security.core.userdetails.User result =
new org.springframework.security.core.userdetails.User(
userName,user.getPassword(), AuthorityUtils.createAuthorityList()
);
return result;
}
}
推荐一个 Spring Boot 基础教程:
PasswordEncoder 是SpringSecurity 的密码解析器,用户密码校验、加密 。 自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象
SpringSecurity 定义了很多实现接口PasswordEncoder 满足我们密码加密、密码校验 使用需求
/**
* 凭证匹配器,用于做认证流程的凭证校验使用的类型
* 其中有2个核心方法
* 1. encode - 把明文密码,加密成密文密码
* 2. matches - 校验明文和密文是否匹配
* */
public class MyMD5PasswordEncoder implements PasswordEncoder {
/**
* 加密
* @param charSequence 明文字符串
* @return
*/
@Override
public String encode(CharSequence charSequence) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return toHexString(digest.digest(charSequence.toString().getBytes()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
/**
* 密码校验
* @param charSequence 明文,页面收集密码
* @param s 密文 ,数据库中存放密码
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(encode(charSequence));
}
/**
* @param tmp 转16进制字节数组
* @return 饭回16进制字符串
*/
private String toHexString(byte [] tmp){
StringBuilder builder = new StringBuilder();
for (byte b :tmp){
String s = Integer.toHexString(b & 0xFF);
if (s.length()==1){
builder.append("0");
}
builder.append(s);
}
return builder.toString();
}
}
2.在配置类中指定自定义密码凭证匹配器
/**
* 加密
* @return 加密对象
* 如需使用自定义密码凭证匹配器 返回自定义加密对象
* 例如: return new MD5PasswordEncoder();
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //Spring Security 自带
}
http.formLogin()
.usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
.passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
.loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
.loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
.failureForwardUrl("/failure"); // 登录失败后,请求转发的位置。Security请求转发使用Post请求。默认转发到: loginPage?error
.successForwardUrl("/toMain"); // 用户登录成功后,请求转发到的位置。Security请求转发使用POST请求。
http.formLogin()
.usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
.passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
.loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
.loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
.defaultSuccessUrl("/toMain",true); //用户登录成功后,响应重定向到的位置。 GET请求。必须配置绝对地址。
.failureUrl("/failure"); // 登录失败后,重定向的位置。
/*自定义登录失败处理器*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
private boolean isRedirect;
public MyAuthenticationFailureHandler(String url, boolean isRedirect) {
this.url = url;
this.isRedirect = isRedirect;
}
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
if (isRedirect){
httpServletResponse.sendRedirect(url);
}else {
httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
}
}
//get set 方法 省略
/**
* 自定义登录成功后处理器
* 转发重定向,有代码逻辑实现
* */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
private boolean isRedirect;
public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {
this.url = url;
this.isRedirect = isRedirect;
}
/**
* @param request 请求对象 request.getRequestDispatcher.forward()
* @param response 响应对象 response.sendRedirect()
* @param authentication 用户认证成功后的对象。其中报换用户名权限结合,内容是
* 自定义UserDetailsService
* */
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (isRedirect){
response.sendRedirect(url);
}else {
request.getRequestDispatcher(url).forward(request,response);
}
}
//get set 方法 省略
http.formLogin()
.usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
.passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
.loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
.loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserSecurity userSecurity;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
/**
* 加密
* @return 加密对象
* 如需使用自定义加密逻辑 返回自定义加密对象
* return new MD5PasswordEncoder(); return new SimplePasswordEncoder();
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //Spring Security 自带
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录请求相关内容。
http.formLogin()
.loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
.usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
.passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
.loginProcessingUrl("/login") //设置登录 提交表单数据访问请求地址
.defaultSuccessUrl("/toMain")
.failureUrl("/toLogin");
//.successForwardUrl("/toMain")
//.failureForwardUrl("/toLogin");
//.successHandler(new LoginSuccessHandler("/toMain", true)) //自定义登录成功处理器
//.failureHandler(new LoginErrorHandler("/toLogin", true));
http.authorizeRequests()
//.antMatchers("/toLogin").anonymous() //只能匿名用户访问
.antMatchers("/toLogin", "/register", "/login", "/favicon.ico").permitAll() // /toLogin请求地址,可以随便访问。
.antMatchers("/**/*.js").permitAll() // 授予所有目录下的所有.js文件可访问权限
.regexMatchers(".*[.]css").permitAll() // 授予所有目录下的所有.css文件可访问权限
.anyRequest().authenticated(); // 任意的请求,都必须认证后才能访问。
// 配置退出登录
http.logout()
.invalidateHttpSession(true) // 回收HttpSession对象。退出之前调用HttpSession.invalidate() 默认 true
.clearAuthentication(true) // 退出之前,清空Security记录的用户登录标记。 默认 true
// .addLogoutHandler() // 增加退出处理器。
.logoutSuccessUrl("/") // 配置退出后,进入的请求地址。 默认是loginPage?logout
.logoutUrl("/logout"); // 配置退出登录的路径地址。和页面请求地址一致即可。
// 关闭CSRF安全协议。
// 关闭是为了保证完整流程的可用。
http.csrf().disable();
}
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
hasAuthority(String) 判断角色是否具有特定权限
http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")
hasAnyAuthority(String ...) 如果用户具备给定权限中某一个,就允许访问
http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx")
hasRole(String) 如果用户具备给定角色就允许访问。否则出现403
//请求地址为/admin/read的请求,必须登录用户拥有'管理员'角色才可访问
http.authorizeRequests().antMatchers("/admin/read").hasRole("管理员")
hasAnyRole(String ...) 如果用户具备给定角色的任意一个,就允许被访问
//用户拥有角色是管理员 或 访客 可以访问 /guest/read
http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理员", "访客")
hasIpAddress(String) 请求是指定的IP就运行访问
//ip 是127.0.0.1 的请求 可以访问/ip
http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")
1.编写类实现接口AccessDeniedHandler
/**
* @describe 403 权限不足
* @author: AnyWhere
* @date 2021/4/18 20:57
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(
"<html>" +
"<body>" +
"<div style='width:800px;text-align:center;margin:auto;font-size:24px'>" +
"权限不足,请联系管理员" +
"</div>" +
"</body>" +
"</html>"
);
response.getWriter().flush();//刷新缓冲区
}
}
2.配置类中配置exceptionHandling
// 配置403访问错误处理器。
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置记住密码
http.rememberMe()
.rememberMeParameter("remember-me") // 修改请求参数名。 默认是remember-me
.tokenValiditySeconds(14*24*60*60) // 设置记住我有效时间。单位是秒。默认是14天
.rememberMeCookieName("remember-me") // 修改remember me的cookie名称。默认是remember-me
.tokenRepository(persistentTokenRepository) // 配置用户登录标记的持久化工具对象。
.userDetailsService(userSecurity); // 配置自定义的UserDetailsService接口实现类对象
}
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
角色校验 ,请求到来访问控制单元方法时必须包含XX角色才能访问
角色必须添加ROLE_前缀
@Secured({"ROLE_管理员","ROLE_访客"})
@RequestMapping("/toMain")
public String toMain(){
return "main";
}
使用注解@Secured需要在配置类中添加注解 使@Secured注解生效
@EnableGlobalMethodSecurity(securedEnabled = true)
权限检验,请求到来访问控制单元之前必须包含xx权限才能访问,控制单元方法执行前进行角色校验
/**
* [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
* @PreAuthorize 角色 、权限 校验 方法执行前进行角色校验
*
* hasAnyAuthority()
* hasAuthority()
*
* hasPermission()
*
*
* hasRole()
* hasAnyRole()
* */
@PreAuthorize("hasAnyRole('ROLE_管理员','ROLE_访客')")
@RequestMapping("/toMain")
@PreAuthorize("hasAuthority('admin:write')")
public String toMain(){
return "main";
}
使用@PreAuthorize和@PostAuthorize 需要在配置类中配置注解@EnableGlobalMethodSecurity 才能生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
权限检验,请求到来访问控制单元之后必须包含xx权限才能访问 ,控制单元方法执行完后进行角色校验
/**
* [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
* @PostAuthorize 角色 、权限 校验 方法执行后进行角色校验
*
* hasAnyAuthority()
* hasAuthority()
* hasPermission()
* hasRole()
* hasAnyRole()
* */
@PostAuthorize("hasRole('ROLE_管理员')")
@RequestMapping("/toMain")
@PreAuthorize("hasAuthority('admin:write')")
public String toMain(){
return "main";
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack” 或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
通俗解释:
CSRF就是别的网站非法获取我们网站Cookie值,我们项目服务器是无法区分到底是不是我们的客户端,只有请求中有Cookie,认为是自己的客户端,所以这个时候就出现了CSRF。
近期热文推荐:
1.1,000+ 道 Java面试题及答案整理(2022最新版)
4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
觉得不错,别忘了随手点赞+转发哦!
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来
我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion
我似乎经常遇到一些设计问题,但我不知道是什么是真的很合适。一方面我经常听到我应该限制耦合和坚持单一职责,但当我这样做时,我常常发现它很困难到在需要时将信息获取到程序的一部分。为了例如,classSingerdefinitialize(name)@name=nameendattr:nameend那么Song应该是:classSongdefnew(singer)@singer=singerendend或classSongdefnew(singer_name)@singer_name=singer_nameendend后者耦合性小,按道理应该用。但如果我以后发现宋有什么需要了解更多歌手,我的
我需要使用ActiveMerchant库在我们的一个Rails应用程序中设置支付解决方案。尽管这个问题非常主观,但人们对主要网关(BrainTree、Authorize.net等)的体验如何?它必须:处理定期付款。有能力记入个人帐户。能够取消付款。有办法存储用户的付款详细信息(例如Authotize.netsCIM)。干杯 最佳答案 ActiveMerchant很棒,但在过去一年左右的时间里,我在使用它时发现了一些问题。首先,虽然某些网关可能会得到“支持”——但并非所有功能都包含在内。查看功能矩阵以确保完全支持您选择的网关-http
我有一个像这样的ruby散列{"stuff_attributes"=>{"1"=>{"foo"=>"bar","baz"=>"quux"},"2"=>{"foo"=>"bar","baz"=>"quux"}}}我想把它变成一个看起来像这样的散列{"stuff_attributes"=>[{"foo"=>"bar","baz"=>"quux"},{"foo"=>"bar","baz"=>"quux"}]}我还需要保留键的数字顺序,并且键的数量是可变的。上面是super简化的,但我在底部包含了一个真实的例子。执行此操作的最佳方法是什么?附言还需要递归就递归而言,这是我们可以假设的:1)
参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin
如果我构建了一个应用程序来访问来自Gmail、Twitter和Facebook的一些数据,并且我希望用户只需输入一次他们的身份验证信息,并且在几天或几周后重置,那会怎样是在Ruby中动态执行此操作的最佳方法吗?我看到很多人只是拥有他们客户/用户凭证的配置文件,如下所示:gmail_account:username:myClientpassword:myClientsPassword这看起来a)非常不安全,b)如果我想为成千上万的用户存储此类信息,它就无法工作。推荐的方法是什么?我希望能够在这些服务之上构建一个界面,因此每次用户进行交易时都必须输入凭据是不可行的。