阅读提醒
本文是基于Springboot3.0.1和Springsecurity6.0版本,阅读时请注意。
前言
Springboot升级到3.0以后,认证与授权SpringSecurity也就升到6.0了,有些写法已经跟以前的版本不太一样了。对于老手不适合阅读本文,对你没有什么帮助,但对于新手来说还是很有指导意义。
两个关键点
1.重写安全过虑配置
新版本已经放弃了WebSeucrityConfigurerAdapter,不能再用老版本的继承方法了。新版本采用的是重写SecurityFilterChain,代码如下:
package security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((authorize)->authorize
.requestMatchers("/login-form","/login-process").permitAll()//无需授权即可访问的url,多个地址可以这样写。
.requestMatchers("/company").permitAll()//也可以写一个地址。
.anyRequest().authenticated())//其它页面需要授权才可以访问。
.formLogin(form->form
.loginPage("/login-form")//自定义的表单,可以不用框架给的默认表单。
.loginProcessingUrl("/login.do")//这个地址就是摆设,给外人看的,只要跟form表单的action一致就好,真正起作用的还是UserDetailsService。
.permitAll()
//.successForwardUrl("/"))
.defaultSuccessUrl("/"))//建议用这个,successForwardUrl只能返回指定页,而这个可以返回来源页,没有来源页才会返回指定页,体验较好。
.logout()//注销登录
.logoutUrl("/logout")//这个地址也只是给人看的,只要跟前端注销地址一致就可。
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"))//主要是指定注销时用什么请求方法,GET和POST都可以吧。
.logoutSuccessUrl("/login-form")//注销后要跳转的页面,那个页面一定是不需要授权就可以访问的,否则会出现意外结果。一般是首页,这里随便写的一个页面。
.permitAll()
.and()
.exceptionHandling().accessDeniedPage("/unAuth.html");//自定义没权限访问的提示页面。
return http.build();
}
//密码加密用的,不然没法做密码比对。
@Bean
public PasswordEncoder getPwdEncoder(){
return new BCryptPasswordEncoder();
}
}
以上代码可以直接用,只要改一下里面自定义的url即可,改成你自己的。
2.重写UserDetailsService
UserDetailsService是SpringSecurity与认证数据交互的地方,如果你不重写UserDetailsService,那么SpringSecurity认证的过程就是走的自己内部默认的那一套,只能是在application.properties的配置里写死帐号密码和角色,这显然不能用于生产环境,生产环境自然要用数据库里的数据进行用户认证和获得相关权限,代码如下:
package security.service.impl;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import security.model.Authority;
import security.model.Role;
import security.service.UserService;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//通过构造方法,将我们获取数据库的数据的接口服务装配到类中,供后面获取数据。
//也可以用注解@Autowired来自动装配,只是总报提示建议用构造方法,作为有强迫症的我就用了构造方法。
private final UserService userService;
public UserDetailsServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException。
//这里引用的security.model.User就是我们自己定义的实体类,与我们的数据库对应。
List<security.model.User> users = userService.loadUserByUsername(username);
//判断帐号是否存在,如果查不到,SpringSecurity不会让你认证通过,就是利用UsernameNotFoundException来实现的。
//我的数据库里都没有你的帐号,怎么会让你通过呢?
if(users.size() == 0 || username.isEmpty()){
throw new UsernameNotFoundException("用户名不存在");
}
//这样就从数据库查到我们的用户信息啦,将这些信息注入我们的实体类中。
security.model.User user = users.get(0);
//System.out.println(username);
//从实体类中获取用户的密码。
String password = user.getPassword();
//System.out.println(password);
//收集角色和权限,一个用户可以对应多个角色,一个角色可以对应多个权限(比如访问某个菜单或方法的权限)
//你可以定义一个角色表,一个权限表。同时要建一个用户到角色的关联表和角色到权限的关联表。这些比较简单,这里不再赘述。
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
//获取用户角色。
List<Role> roles = userService.loadRoleByUid(user.getId());
for(Role role:roles){
//角色前一定要加上"ROLE_"前缀,否则SpringSecurity会视为无效,它是通过这个前缀识别角色的。
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_".concat(role.getName()));
grantedAuthorityList.add(simpleGrantedAuthority);
//System.out.println(role.getName());
}
//获取用户各模块或菜单的权限。
List<Authority> authorities = userService.loadAuthorityByUid(user.getId());
for(Authority authority:authorities){
//权限不需要什么前缀,比较省心。
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority.getName());
grantedAuthorityList.add(simpleGrantedAuthority);
//System.out.println(authority.getName());
}
//以下是做授未用数据库时测试用的,我是先测试通过了下面的代码,又开始连接数据库的,你可以不用理会,但也有参考价值。
/*if(!username.eq("admin")){
throw new UsernameNotFoundException("用户名不存在");
}*/
//密码一定要用BCryptPasswordEncoder加密过的,否则认证不通过,下面的乱码就是123456加密而来的,吼吼。
//String password = "$2a$10$UUbMgKe4ozOFfcfJcC6KoOQHLixmMNF.Evx.E5/AkidUExBGXuq6m";
//grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));//一定要加"ROLE_"前缀,否则无效。
//grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_USER"));//每个用户最低要有一个USER角色。
//grantedAuthorityList.add(new SimpleGrantedAuthority("CART"));//这是添加权限,不需要什么前缀。
//来啦来啦,关键的关键来啦,要把我们收集到的用户信息一股脑的捅进SpringSecurity容器里。
//SpringSecurity会自动验证你的用户密码是否正确,如果密码不正确就会返回登录页,正确就把用户信息注入内存以备用。
return new User(username,password, grantedAuthorityList);
}
}
重写UserDetailsService,是架起与数据库沟通的桥梁,这里弄明白了,后面都很简单了。尤其是最后那句ruturn new User(username,password,grantedAuthorityList),简直不要太酸爽!就是它完成了最后的数据注入。切记,此处的这个User并不是我们model下自定义的那个实体类,而是SpringSecurity框架自带的,千万不要搞错了。
前端控制器示例
这些代码随便看看就好,都是测试用的,主要是看看角色和权限如何用在授权权限上。主要是注解@PreAuthorize的使用上,怎么用角色,又怎么用权限,使用的时候这两个没有太大的区别。为什么还要分角色和权限呢?主要还是为了方便管理。人可以有多个角色,一个角色可以有多个权限,管理起来非常方便。但在代码里,都是一样的认证。
package security.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import security.model.User;
import security.service.UserService;
import java.util.List;
@Controller
@EnableMethodSecurity
public class IndexController {
//@Autowired
//UserMapper userMapper;
@Autowired
UserService userService;
@RequestMapping("/")
public String index(){
//UserService userService = new UserServiceImpl(userMapper);
List<User> users = userService.loadUserByUsername("admin");
System.out.println(users.get(0).getPassword());
return "index";
}
@RequestMapping("/login-form")
public String loginForm(){
return "login-form";
}
@RequestMapping("/login-process")
@ResponseBody
public String loginProcess(){
return "login-process";
}
@RequestMapping("/company")
@ResponseBody
public String company(){
return "company";
}
@RequestMapping("/product")
@ResponseBody
@PreAuthorize("hasRole('USER')")//以角色身份访问。
public String product(){
return "product";
}
@RequestMapping("/price")
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")//如果以角色身份访问,不需要在前面加"ROLE_"前缀。
public String price(){
return "price";
}
@RequestMapping("/cart")
@ResponseBody
@PreAuthorize("hasAuthority('SHEQU')")//以权限身份访问。
public String cart(){
return "我的购物车!";
}
}
总结
本文切中要害,直接把安全认证的两个关键点找了出来,让大家明白自定登录我要从哪里入手。第一是重写SecurityFilterChain,第二是重写UserDetailsService,如何使用很简单,就是注解@PreAuthorize放在哪里的问题。
我正在尝试设置一个puppet节点,但rubygems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由rubygems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby中使用两个参数异步运行exe吗?我已经尝试过ruby命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何rubygems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby1.9+ 关于ruby-主要:Objectwhenrun
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin
我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano
我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>
我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c
我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是