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>
二、其他配置
- 像
mybatis-plus
以及一些常量工具类的配置描述就不再展开,查看源码即可。
三、自定义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)));
}
}
并发登录会话失效处理类
/**
* @version 1.0.0
* @className: CustomExpiredSessionStrategyImpl
* @description: 自定义并发会话失效策略
* @author: LiJunYi
* @create: 2022/7/26 11:53
*/
@Component
public class CustomExpiredSessionStrategyImpl implements SessionInformationExpiredStrategy , Serializable
{
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException
{
int code = HttpStatus.CONFLICT;
String msg = "您的账号已经在其它设备登录,如果非本人操作,请立即修改密码!";
HttpServletResponse response = event.getResponse();
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
退出处理类
/**
* @version 1.0.0
* @className: LogoutSuccessHandlerImpl
* @description: 退出处理器
* @author: LiJunYi
* @create: 2022/7/26 8:34
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) throws IOException {
boolean isAjaxRequest = ServletUtils.isAjaxRequest(request);
if (isAjaxRequest)
{
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
}else
{
response.sendRedirect("/login");
}
}
}
四、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();
}
}
五、自定义Model实现UserDetails
/**
* @version 1.0.0
* @className: LoginUser
* @description: 自定义用户对象并实现UserDetails
* @author: LiJunYi
* @create: 2022/7/26 12:20
*/
public class LoginUser implements UserDetails
{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 登录时间
*/
private Long loginTime;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getDeptId() {
return deptId;
}
public void setDeptId(Long deptId) {
this.deptId = deptId;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public SysUser getUser() {
return user;
}
public void setUser(SysUser user) {
this.user = user;
}
public LoginUser()
{
}
public LoginUser(SysUser user, Set<String> permissions)
{
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
{
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
}
六、自定义实现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));
}
}
七、获取登录用户权限
/**
* @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());
}
}
八、自定义权限校验注解
/**
* @version 1.0.0
* @className: PermissionService
* @description: 自定义的权限校验,代码来源(RuoYi-Vue)项目
* @author: LiJunYi
* @create: 2022/7/26 15:38
*/
@Service("ss")
public class PermissionService
{
/** 所有权限标识 */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理员角色权限标识 */
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission)
{
if (StrUtil.isEmpty(permission))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 验证用户是否不具备某权限,与 hasPermi逻辑相反
*
* @param permission 权限字符串
* @return 用户是否不具备某权限
*/
public boolean lacksPermi(String permission)
{
return hasPermi(permission) != true;
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
* @return 用户是否具有以下任意一个权限
*/
public boolean hasAnyPermi(String permissions)
{
if (StrUtil.isEmpty(permissions))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
Set<String> authorities = loginUser.getPermissions();
for (String permission : permissions.split(PERMISSION_DELIMETER))
{
if (permission != null && hasPermissions(authorities, permission))
{
return true;
}
}
return false;
}
/**
* 判断用户是否拥有某个角色
*
* @param role 角色字符串
* @return 用户是否具备某角色
*/
public boolean hasRole(String role)
{
if (StrUtil.isEmpty(role))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (SysRole sysRole : loginUser.getUser().getRoles())
{
String roleKey = sysRole.getRoleKey();
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StrUtil.trim(role)))
{
return true;
}
}
return false;
}
/**
* 验证用户是否不具备某角色,与 isRole逻辑相反。
*
* @param role 角色名称
* @return 用户是否不具备某角色
*/
public boolean lacksRole(String role)
{
return !hasRole(role);
}
/**
* 验证用户是否具有以下任意一个角色
*
* @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
* @return 用户是否具有以下任意一个角色
*/
public boolean hasAnyRoles(String roles)
{
if (StrUtil.isEmpty(roles))
{
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (String role : roles.split(ROLE_DELIMETER))
{
if (hasRole(role))
{
return true;
}
}
return false;
}
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set<String> permissions, String permission)
{
return permissions.contains(ALL_PERMISSION) || permissions.contains(StrUtil.trim(permission));
}
}
九、登录接口以及业务代码
/**
* 登录控制器
*
* @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";
}
}
/**
*
* 系统用户 前端控制器
*
*
* @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 "删除成功";
}
}
十、测试示例图
以上就是 SpringBoot
接入 SpringSecurity
(v5.7.x) 的步骤。
唠嗑
最开始接入的时候,问题还挺多,特别是 securityConfig
配置类这块,虽说变化不大,但是因为官方文档内容比较散,有些配置冲突或者多余也没有标记,还是得自己一步步踩坑过去才知道哪些才是对的配置。
项目源码中,PermissionService
类来源于 RuoYi-Vue
项目。当然你也可以自己实现权限的校验,毕竟登录的时候,权限已经绑定在 loginUser
对象中,只要判断是否包含注解带入的参数即可。
后面一章节将整合jwt
,也就是自定义登录过滤器来实现登录,文中有错误的地方希望多多指正。