阅读提醒
本文是基于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 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 grantedAuthorityList = new ArrayList<>();
//获取用户角色。
List 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 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 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放在哪里的问题。