更新记录
20240319:第一版
开篇
中文文档: https://springdoc.cn/spring-security/servlet/getting-started.html
推荐文章: https://shusheng007.top/2023/02/15/springsecurity/
推荐文章: https://juejin.cn/post/7212616585768714299
推荐把上面 2 个推荐的文章简单浏览一遍, 该篇在 https://github.com/eezd/cli-template/tree/main/springboot2-template 该项目的基础上进行讲解
主要对实现流程进行梳理
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.5</version>
</dependency>
流程图
身份认证(authentication),即验证用户身份的合法性,以判断用户能否登录。
授权(authorization),即验证用户是否有权限访问某些资源或者执行某些操作。
下面这张图就是 Spring Security 的流程图了, 现在你会觉得不理解, 没关系, 下面我会结合代码进行解释
- Filter:
UsernamePasswordAuthenticationFilter
- 拦截 Http 请求,获取用户名和秘密等认证信息
- AuthenticationManager:
ProviderManager
- 从 filter 中获取认证信息,然后查找合适的 AuthenticationProvider 来发起认证流程
- AuthenticationProvider:
DaoAuthenticationProvider
- 调用 UserDetailsService 来查询已经保存的用户信息并与从 http 请求中获取的认证信息比对。如果成功则返回,否则则抛出异常。
- UserDetailsService:
InMemoryUserDetailsManager
- 负责获取用户保存的认证信息,例如查询数据库。
- Filter:
基础实现
LoginController
- 我们定义登录
Controller
// com.eezd.main.web.system.controller.SysLoginRegisterController.java
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody) {
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(
loginBody.getUsername(),
loginBody.getPassword()
);
ajax.put(Constants.TOKEN, token);
return ajax;
}
SysLoginService
- 第一步: 我们先封装好
Authentication
- 第二步: 设置联系上下文, 之后我们才能获取到用户密码
- 第三步: 进行登录验证, 看看用户在不在, 以及配置权限/角色信息
// com.eezd.main.core.service.SysLoginService.java
public String login(String username, String password) {
// 用户验证
Authentication authentication = null;
// 第一步: 封装请求信息为 Authentication
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 第二步: 设置联系上下文, 后续方便后面获取信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 第三步: 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
UserDetailsServiceImpl
- 第一步: 查看数据库中是否存在这个用户
- 第二步: 通过
SecurityContextHolder
获取用户密码- 我们在
1.2
的第二步设置了联系上下文了
- 我们在
- 最后你可以进行以一列的校验, 判断用户名与密码是否正确
// com.eezd.main.core.service.UserDetailsServiceImpl.java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 第一步: 获取数据库 user
QueryWrapper<SysUser> user_queryWrapper = new QueryWrapper<>();
user_queryWrapper.eq("user_name", username);
SysUser user = sysUserMapper.selectOne(user_queryWrapper);
// 第二步: 获取用户密码
Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
String username2 = usernamePasswordAuthenticationToken.getName();
String password = usernamePasswordAuthenticationToken.getCredentials().toString();
return (UserDetails) user;
}
}
config 配置
在 Spring Security 5.7 以上时,
WebSecurityConfigurerAdapter
已经被弃用了, 具体解决办法请看第七章
- 这里你看着注解修改, 我不在说明
/**
* spring security配置
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage", "/redis-test").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
权限/角色校验
表设计
- 角色表这里就不放出来了, 但是你必须要清楚, 角色必须是
ROLE_
开头- 角色必须是
ROLE_
开头 - 角色必须是
ROLE_
开头 - 角色必须是
ROLE_
开头
- 角色必须是
create table sys_permission
(
premission_id bigint auto_increment comment '权限ID'
primary key,
premission_name varchar(50) not null comment '权限名称',
perms varchar(100) null comment '权限标识',
remark varchar(500) default '' null comment '备注'
)comment '权限表';
insert into sys_permission (premission_id, premission_name, perms, remark) values (1000, '用户查询', 'system:user:query', '');
/**
* 查询角色相关权限
*/
@Data
public class RolePermissionVO implements Serializable {
/**
* 角色ID
*/
@TableId(type = IdType.AUTO)
private Long roleId;
/**
* 角色名称
*/
private String roleName;
/**
* 角色权限
*/
private String roleKey;
/**
* 角色排序
*/
private Integer roleSort;
/**
* 角色状态(0正常 1停用)
*/
private String status;
/**
* 删除标志(0代表存在 2代表删除)
*/
private String delFlag;
/**
* 创建人
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 备注
*/
private String remark;
private List<SysPermission> sysPermission;
}
代码
package com.eezd.main.core.service;
/**
* 用户验证处理
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws
//...
// 定义权限
List<SimpleGrantedAuthority> permissions = new ArrayList<>();
RolePermissionVO userRoleQuery = sysRoleMapper.selectRolePermission(user.getUserId());
// 管理员则添加所有权限
if (user.getUserId() == 1L) {
userRoleQuery.setSysPermission(sysPermissionMapper.selectList(null));
}
// 添加角色
permissions.add(new SimpleGrantedAuthority("ROLE_".concat(userRoleQuery.getRoleKey())));
// 添加权限
List<SysPermission> userPermissions = userRoleQuery.getSysPermission();
if (userPermissions != null && !userPermissions.isEmpty()) {
for (SysPermission permission : userPermissions) {
permissions.add(new SimpleGrantedAuthority(permission.getPerms()));
}
}
return createLoginUser(user, permissions);
}
public UserDetails createLoginUser(SysUser user, List<SimpleGrantedAuthority> permissions) {
return new LoginUser(user.getUserId(), user, permissions);
}
}
使用
@PreAuthorize("hasRole('admin')")
@GetMapping("/admin")
public AjaxResult admin() {
AjaxResult ajax = AjaxResult.success("admin");
return ajax;
}
@PreAuthorize("hasAuthority('system:user:query')")
@GetMapping("/system/user/query")
public AjaxResult systemUserQuery() {
AjaxResult ajax = AjaxResult.success("/system/user/query");
return ajax;
}