前述
随着 Spring Boot 2.7.0 的发布,Spring Security 同样也升级到了 5.7.1,升级之后,原有的WebSecurityConfigurerAdapter 方法正式被弃用了,但是主要玩法变化不大。这几天正在以新版Spring Security为基础,记录一下Spring Security的使用过程以及步骤。
本文对应的源代码已经上传到 Gitee,对应项目为 simple ,登录方式为表单登录
后续会持续记录基于 JWT,微服务 等场景的实例代码。
下面将主要的部分贴出,完整内容请查看
基于表单登录的实现
一、引入依赖
<!-- SpringBoot的版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--对Thymeleaf添加Spring Security标签支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
</dependencies>
二、自定义security处理器
这里贴出主要的处理类,完整请查看源码
/**
* @version 1.0.0
* @className: AuthenticationEntryPointImpl
* @description: 认证失败的处理类
* @author: LiJunYi
* @create: 2022/7/25 17:39
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
{
int code = HttpStatus.UNAUTHORIZED;
String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
/**
* @version 1.0.0
* @className: CustomAuthenticationSuccessImpl
* @description: 登录成功处理类
* @author: LiJunYi
* @create: 2022/7/26 13:30
*/
@Component
public class CustomAuthenticationSuccessImpl implements AuthenticationSuccessHandler
{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) {
String msg = "登录成功";
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(msg)));
}
}
三、SecurityConfig配置(新)
/**
* @version 1.0.0
* @className: SecurityConfig
* @description: SpringSecurity 5.7.x新用法配置
* @author: LiJunYi
* @create: 2022/7/26 8:43
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
/**
* 自定义用户登录处理逻辑
*/
private final MyUserDetailsServiceImpl userDetailsService;
/**
* 登录成功处理
*/
private final CustomAuthenticationSuccessImpl customAuthenticationSuccess;
/**
* 注销成功后处理
*/
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 未经授权处理程序
*/
private final AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 并发登录控制处理
*/
private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;
/**
* 构造函数注入
*
* @param userDetailsService 用户详细信息服务
* @param logoutSuccessHandler 注销成功处理程序
* @param unauthorizedHandler 未经授权处理程序
* @param expiredSessionStrategy 过期会话策略
* @param customAuthenticationSuccess 自定义身份验证成功
*/
@Autowired
public SecurityConfig(MyUserDetailsServiceImpl userDetailsService,LogoutSuccessHandlerImpl logoutSuccessHandler,
AuthenticationEntryPointImpl unauthorizedHandler,
CustomExpiredSessionStrategyImpl expiredSessionStrategy,
CustomAuthenticationSuccessImpl customAuthenticationSuccess) {
this.userDetailsService = userDetailsService;
this.logoutSuccessHandler = logoutSuccessHandler;
this.unauthorizedHandler = unauthorizedHandler;
this.expiredSessionStrategy = expiredSessionStrategy;
this.customAuthenticationSuccess = customAuthenticationSuccess;
}
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
return configuration.getAuthenticationManager();
}
/**
* 配置加密方式
*
* @return {@link PasswordEncoder}
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
* 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
* @return {@link HttpSessionEventPublisher}
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
/**
* 过滤器链
*
* @param http http
* @return {@link SecurityFilterChain}
* @throws Exception 异常
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
http.authorizeRequests(authorize ->
authorize.mvcMatchers("/login","/userLogin","/noPermission","/static/css/**","/static/util/javascript/**").permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/userLogin")
.successHandler(customAuthenticationSuccess)
.permitAll());
// 退出过滤器
http.logout(logout -> logout
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(logoutSuccessHandler));
// 认证失败处理类
http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
// 并发会话控制
http.sessionManagement(session -> session
.maximumSessions(1)
.expiredSessionStrategy(expiredSessionStrategy));
http.userDetailsService(userDetailsService);
return http.build();
}
}
四、自定义实现UserDetailsService
/**
* @version 1.0.0
* @className: MyUserDetailsServiceImpl
* @description: security具体登录逻辑
* @author: LiJunYi
* @create: 2022/7/26 9:11
*/
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(MyUserDetailsServiceImpl.class);
private final ISysUserService userService;
private final SysPermissionService permissionService;
/**
* 构造器注入
*
* @param userService 用户服务
* @param permissionService 许可服务
*/
public MyUserDetailsServiceImpl(ISysUserService userService, SysPermissionService permissionService)
{
this.userService = userService;
this.permissionService = permissionService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.getByUsername(username);
if (ObjectUtil.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
// 获取角色
List<SysRole> roles = permissionService.getRolesByUserId(user);
user.setRoles(roles);
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(Convert.toLong(user.getId()), 1000L, user, permissionService.getMenuPermission(user));
}
}
五、SysPermissionService
/**
* @version 1.0.0
* @className: SysPermissionService
* @description:
* @author: LiJunYi
* @create: 2022/7/26 12:36
*/
@Component
public class SysPermissionService
{
private final ISysRoleService roleService;
private final ISysResourceService resourceService;
/**
* 构造器注入
*
* @param roleService 角色服务
* @param resourceService 资源服务
*/
public SysPermissionService(ISysRoleService roleService, ISysResourceService resourceService) {
this.roleService = roleService;
this.resourceService = resourceService;
}
/**
* 获取角色数据权限
*
* @param user 用户信息
* @return 角色权限信息
*/
public Set<String> getRolePermission(SysUser user)
{
return roleService.getRolePermissionByUserId(user.getId());
}
/**
* 获取菜单数据权限
*
* @param user 用户信息
* @return 菜单权限信息
*/
public Set<String> getMenuPermission(SysUser user)
{
List<SysResource> resources = resourceService.listResourceByUserId(user.getId());
List<String> perms = resources.stream().map(SysResource::getPermission).collect(Collectors.toList());
Set<String> permsSet = new HashSet<>();
for (String perm : perms)
{
if (StrUtil.isNotEmpty(perm))
{
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
}
return permsSet;
}
/**
* 获取角色通过用户id
*
* @param user 用户
* @return {@link List}<{@link SysRole}>
*/
public List<SysRole> getRolesByUserId(SysUser user)
{
return roleService.getRolesByUserId(user.getId());
}
}
六、业务接口
/**
* 登录控制器
*
* @author LiJunYi
* @date 2022/07/26
*/
@Controller
public class LoginController
{
/**
* 身份验证管理器
*/
@Autowired
private AuthenticationManager authenticationManager;
/**
* 登录页面
*
* @return {@link String}
*/
@GetMapping("login")
public String login() {
return "login";
}
/**
* 登录方法
*
* @param user 用户
* @return {@link AjaxResult}
*/
@PostMapping("userLogin")
@ResponseBody
public AjaxResult login(SysUser user)
{
if (StrUtil.isEmpty(user.getUsername()) || StrUtil.isEmpty(user.getPassword()))
{
return AjaxResult.error("用户名或密码未输入!");
}
// 用户验证
Authentication authentication = null;
try {
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
}catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
throw new ServiceException("密码错误");
}
else
{
throw new ServiceException(e.getMessage());
}
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return AjaxResult.success();
}
/**
* 首页
*
* @return {@link String}
*/
@GetMapping("index")
public String indexPage()
{
return "index";
}
/**
* 无权限页面
*
* @return {@link String}
*/
@PostMapping("noPermission")
public String noPermissionHtml()
{
return "noPermission";
}
}
/**
* <p>
* 系统用户 前端控制器
* </p>
*
* @author LiJunYi
* @since 2020-05-25
*/
@Controller
@RequestMapping("/sys/user")
public class SysUserController {
@Resource
private ISysUserService userService;
@PreAuthorize("@ss.hasPermi('sys:user:view')")
@GetMapping("list")
public String list(Model model) {
model.addAttribute("userList", userService.list());
LoginUser user = SecurityUtils.getLoginUser();
System.out.println("SecurityUtils.getLoginUser===" + user);
return "user_list";
}
@GetMapping("/add")
@ResponseBody
@PreAuthorize("@ss.hasPermi('sys:user:add')")
public String add() {
return "跳转新增页面成功";
}
@GetMapping("/edit/{id}")
@PreAuthorize("@ss.hasPermi('sys:user:edit')")
@ResponseBody
public String edit(@PathVariable Long id) {
return "跳转修改页面成功";
}
@GetMapping("/del/{id}")
@PreAuthorize("@ss.hasPermi('sys:user:del')")
@ResponseBody
public String del(@PathVariable Long id) {
return "删除成功";
}
}
七、测试效果图
以上只是将关键地方的代码贴出,完整的请查看源码哦~