有哪些安全框架:
shiro:轻量级、概念简单、配置简单、功能简单
SpringSecurity:重量级、概念复杂、配置繁琐、功能强大
安全框架的主要功能:认证、授权、每个用户的浏览权限不同。
token缺点:发放一个令牌后,在有效期内无法销毁。
解决方案:我们可以通过存到redis里面的一个废弃token来表示某个token销毁了。
token放到客户端的localStorage本地存储里,不是cookie里,访问页面的时候,前端程序员手动把token加到url里。
authentication:认证。该用户是不是本系统的用户。
authorization:授权。该用户是否有权限执行某个操作。
RBAC模型:定义一个角色,角色有一定的权限,用户属于某个角色。
最少5张表:用户表、角色表、用户角色表、权限表、角色权限表。
UsernamePasswordAuthenticationFilter:负责处理登录请求,入门案例中使用到了它。
ExceptionTranslationFilter:负责过滤器中抛出的异常
AccessDeniedException和AuthenticationException
FilterSecurityInterceptor:负责权限校验的过滤器
导包:
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
权限系统:
不同的用户拥有不同的角色权限。
SpringSecurity角色方案一般用注解方式。
jwt里面保存了用户的id,用户的详细信息保存在redis里,通过jwt里的用户id查redis里的用户信息。
封装用户信息:
Authentication
UsernamePasswordAuthenticationToken
RememberMeAuthenticationToken
定义用户信息的来源:
UserDetailsManager
InMemoryUserDetailsManager
JdbcUserDetailsManager
用户身份信息:
UserDetails
User
一般我们自定义一个类实现它
WebSecurityConfigurerAdapter(AuthenticationManager)
.authenticate()->调用
ProviderManager.
authenticate.(Authentication authentication)
DaoAuthenticationProvider.retrieveUser()->调用
UserDetailsServiceImpl.loadUserByUsername()
需要编写的代码:
编写Config类 配置类 实现WebSecurityConfigurerAdapter
@Configuration
// 开启鉴权
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtFilter;
// 认证失败
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
// 授权失败
@Autowired
private AccessDeniedHandler accessDeniedHandler;
// 自动加密验证
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 登录
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 关闭csrf
http.csrf().disable()
// 不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/login").anonymous()
// 除了上面以外的所有接口都需要鉴权认证
.anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
// 认证失败
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// 授权失败
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
// 允许跨域
http.cors();
}
}
编写LoginUser类 用户信息类 实现UserDetails
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser user;
private List<String> permissions;
// 不序列化
@JsonIgnore
private List<GrantedAuthority> authority = null;
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
/*List<GrantedAuthority> newList = new ArrayList<>();
for (String permission : permissions) {
GrantedAuthority authority = new SimpleGrantedAuthority(permission);
newList.add(authority);
}
*/
if (authority == null) {
authority = permissions.stream()
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
return authority;
}
@Override
@JsonIgnore
public String getPassword() {
return user.getPassword();
}
@Override
@JsonIgnore
public String getUsername() {
return user.getUserName();
}
// true:账号未过期
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}
// true:账号未锁定
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}
// true:凭证未过期
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
// ture:账号可用
@Override
@JsonIgnore
public boolean isEnabled() {
return true;
}
}
编写LoginServiceImpl类 登录验证类 实现LoginService
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Result login(User user) {
Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authenticate = null;
try {
authenticate = authenticationManager.authenticate(authentication);
} catch (Exception e) {
return Result.err(e.getMessage());
}
if (authenticate == null) {
throw new RuntimeException("登录失败");
}
// 用户
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
// redis
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
ops.set("login:" + userId, loginUser, 1, TimeUnit.DAYS);
// token
String token = JwtUtil.setJwtToken(userId);
HashMap<String, String> map = new HashMap<>();
map.put("token", token);
return new Result(200, "登录成功", map);
}
@Override
public Result logout() {
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
// 用户
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Integer userId = Math.toIntExact(loginUser.getUser().getId());
redisTemplate.delete("login:"+userId);
return new Result(200,"退出成功",null);
}
}
编写UserDetailsServiceImpl类 用户身份鉴权类 实现UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired(required = false)
private SysUserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
// 获取用户的权限
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!StringUtils.hasText(username)) return null;
// 查询用户信息
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUserName, username);
SysUser user = userMapper.selectOne(wrapper);
if (user == null) throw new RuntimeException("用户不存在");
// 查询用户的权限信息
List<String> list = menuMapper.selectPermsByUserId(user.getId());
return new LoginUser(user, list, null);
}
}
编写JwtAuthenticationTokenFilter拦截器 实现OncePerRequestFilter'
@Component
// 鉴权
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Autowired
private ObjectMapper om;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// filterChain.doFilter(request,response);
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
// jwt
String userId = null;
try {
userId = JwtUtil.getJwtToken(token);
} catch (ExpiredJwtException e) {
// response.getWriter().write(om.writeValueAsString(Result.err("token过期了")));
// response.setContentType("text/html;charset=utf-8");
// response.getWriter().write("ABC");
throw new RuntimeException("token过期了");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// redis
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
LoginUser loginUser = (LoginUser) ops.get("login:" + userId);
/*if (loginUser == null) {
throw new RuntimeException("用户未登录");
}*/
// TODO 获取用户的权限信息
if (loginUser != null) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}
案例:
这个接口需要删除书的权限:
@GetMapping("{id}")
@PreAuthorize("hasAuthority('del:book')")
public String delBook(@PathVariable("id") Integer id) {
return "del book success";
}
需求:
用户没权限,需要给予用户提示,借助异常处理机制。
public class WebUtil {
public static void renderString(HttpServletResponse res, String s) {
res.setStatus(200);
res.setContentType("application/json");
res.setCharacterEncoding("utf-8");
try {
res.getWriter().print(s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
自定义认证失败处理:
认证错误:
编写AuthenticationEntryPointImpl类 实现AuthenticationEntryPoint
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper om;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 处理异常
Result<Object> json = new Result<>(HttpStatus.UNAUTHORIZED.value(), "用户认证失败", null);
WebUtil.renderString(response, om.writeValueAsString(json));
}
}
权限不足:
编写AccessDeniedHandlerImpl类 实现AccessDeniedHandler
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Autowired
private ObjectMapper om;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result<Object> json = new Result<>(HttpStatus.FORBIDDEN.value(), "用户权限不足", null);
WebUtil.renderString(response, om.writeValueAsString(json));
}
}
开启跨域:
编写跨域配置类
springboot需要开启、springsecurity也需要开启
// 跨域类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// WebMvcConfigurer.super.addCorsMappings(registry);
// 运行跨域的路径
registry.addMapping("/**")
// 运行跨域请求的域名
.allowedOriginPatterns("localhost")
// cookie
.allowCredentials(true)
// 运行请求方式
.allowedMethods("GET", "POST", "DEL", "PUT")
// 跨域允许时间
.maxAge(3600);
}
}
CSRF漏洞:诱导访问。解决方案:要求客户端访问后端接口时在请求体再携带一个csrf_token。token天然的避免了CSRF攻击。
参考视频:
SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-B站最通俗易懂的Spring Security课程_哔哩哔哩_bilibili
【2023版 B站最新】SpringSecurity5 最新教程 入门+进阶+实战+原理+源码 彻底搞懂SpringSecurity_哔哩哔哩_bilibili