# Shiro
# SpringSecurity
Spring Security 是 Spring 家族中的一个安全管理框架。一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目用Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般的Web应用需要进行认证和授权,是SpringSecurity作为安全框架的核心功能
- 认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作
# 快速入门
搭建一个简单的SpringBoot工程,引入依赖后访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
# 登录校验流程
# 基本原理
SpringSecurity的原理其实是一个过滤器链,内部包含了各种功能的过滤器,如下是几个重要的过滤器
//认证过滤器,处理登录页面的登录请求。默认的账号密码认证主要是他负责
UsernamePasswordAuthenticationFilter
//异常处理过滤器,处理过滤器链抛出的AccessDeniedException和AuthenticationException
ExceptionTranslationFilter
//授权过滤器,负责权限校验的过滤器
FilterSecurityInterceptor
查看完整过滤器链的流程
# 认证流程
Authentication接口
# 表示当前访问系统的用户,封装了前端传入的用户相关信息
AuthenticationManager接口
# 定义了认证的方法
UserDetailsService接口
# 定义了一个根据用户名查询用户信息的方法(默认在内存中),返回UserDetails对象
UserDetails接口
# 提供核心用户信息。将UserDetails对象信息封装到Authentication对象中
# 自定义登录校验流程
登录
# 第一步:自定义UserDetailsService接口,在这个实现类中去查询数据库
# 第二步:自定义登录接口,调用ProviderManager的authenticate方法进行认证
校验
# 第三步:自定义Jwt认证过滤器,根据token中的userid,将redis中的用户信息存入SecurityContextHolder
# 授权流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。
# 第一步:把当前登录用户的权限信息也存入Authentication
# 第二步:设置我们的资源所需要的权限
# 核心代码实现
- 创建一个类实现UserDetailsService接口,重写其中的方法。增加用户名从数据库中查询用户信息
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
// TODO根据用户查询权限信息 添加到LoginUser中
List<String> list = new ArrayList<>(Arrays.asList("test"));
Set<SimpleGrantedAuthority> authoritiesSet = new HashSet<SimpleGrantedAuthority>();
for (String roles : list) {
SimpleGrantedAuthority role = new SimpleGrantedAuthority(roles);
authoritiesSet.add(role);
}
//封装成UserDetails对象返回
return new LoginUser(user,authoritiesSet);
}
}
- UserDetailsService的返回值是UserDetails类型,需要定义一个实现该接口的类,把用户信息封装在其中
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {
private User user;
//权限信息
private Set<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
注意
此时如果你想让用户的密码是明文存储,需要在密码前加{noop}
- 替换PasswordEncoder的加密方式,一般使用SpringSecurity为我们提供的BCryptPasswordEncoder
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 自定义登陆接口,通过AuthenticationManager的authenticate方法来进行用户认证,认证成功返回jwt
@RestController
public class LoginController {
@Autowired
private LoginServcie loginServcie;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginServcie.login(user);
}
}
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//通过AuthenticationManager的authenticate方法来进行用户认证
Authentication authenticate=authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//使用userid生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:"+userId,loginUser.getUser());
//把token响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登陆成功",map);
}
}
- 定义一个认证过滤器,根据请求头中token包含的userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
Object cacheObject = redisCache.getCacheObject(redisKey);
User User = (User)cacheObject;
if (Objects.isNull(User)) {
throw new RuntimeException("用户未登录");
}
// TODO获取权限信息封装到Authentication中
List<String> list = new ArrayList<>(Arrays.asList("test"));
Set<SimpleGrantedAuthority> authoritiesSet = new HashSet<SimpleGrantedAuthority>();
for (String roles : list) {
SimpleGrantedAuthority role = new SimpleGrantedAuthority(roles);
authoritiesSet.add(role);
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(User, null, authoritiesSet);
//存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
- 退出登陆:根据SecurityContextHolder中的认证信息,删除redis中对应的数据即可
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User User = (User) authentication.getPrincipal();
Long userid = User.getId();
redisCache.deleteObject("login:"+userid);
return new ResponseResult(200,"退出成功");
}
- 自定义失败处理,在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到
//认证过程中出现的异常会被封装成AuthenticationException
//然后调用AuthenticationEntryPoint对象的方法去进行异常处理
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response
,AuthenticationException authException)
throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value()
,"认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
//授权过程中出现的异常会被封装成AccessDeniedException
//然后调用AccessDeniedHandler对象的方法去进行异常处理
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response
,AccessDeniedException accessDeniedException)
throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value()
, "权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
- 基于注解的权限控制方案,可以使用注解去指定访问对应的资源所需的权限
//再配置文件中开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController
public class HelloController {
@PreAuthorize("hasAuthority('test')")
//传入多个权限,只要用户有其中任意一个权限都可以访问对应资源
@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
//要求有对应的角色才可以访问,但是会把我们传入的参数拼接上 ROLE_ 再去比较
@PreAuthorize("hasRole('system:dept:list')")
@PreAuthorize("hasAnyRole('admin','system:dept:list')")
//在SPEL表达式中使用 @ex相当于获取容器中bean的名字
@PreAuthorize("@ex.hasAuthority('system:dept:list')")
@RequestMapping("/hello")
public String hello(){
return "hello";
}
}
- SecurityConfig 对象完整内容
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationJwtTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return new AuthenticationEntryPointImpl();
}
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return new AccessDeniedHandlerImpl();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用basic明文验证
.httpBasic().disable()
//前后端分离架构不需要csrf保护
.csrf().disable()
// 禁用默认登录页
.formLogin().disable()
// 禁用默认登出页
.logout().disable()
// 处理认证失败、鉴权失败
.exceptionHandling(ex -> ex.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler()))
//默认的情况下将认证信息放到HttpSession中,前后端分离的情况不需要
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//开始设置权限
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
// 对于登录接口 允许匿名访问
// 配置一个request Mather的string数组,参数为url路径格式
.antMatchers("/user/login").permitAll()
// 配置一个request Mather数组,参数为RequestMatcher 对象
.requestMatchers(a->a.getRequestURI().startsWith("/register")).denyAll()
// 允许错误地址匿名访问
.requestMatchers(a->a.getRequestURI().startsWith("/error")).permitAll()
// 其他所有接口必须有Authority信息,且是admin权限
.antMatchers("/**").hasAnyAuthority("admin")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated())
.authenticationProvider(authenticationProvider())
.addFilterBefore(authenticationJwtTokenFilter()
,UsernamePasswordAuthenticationFilter.class)
//允许跨域,需要先对SpringBoot配置跨域
.cors();
return http.build();
}
/**
* 调用loadUserByUsername获得UserDetail信息
* @return
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
authProvider.setUserDetailsService(userDetailsService());
// 设置密码编辑器
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 登录时需要调用AuthenticationManager.authenticate执行一次校验
* @param config
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
/**
* 忽略某些URL请求
* @return
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
//这些请求将被忽略,这意味着这些URL将有受到 CSRF、XSS、Clickjacking 等攻击的可能
return (web) -> web.ignoring().antMatchers("/images/**", "/js/**");
}
}
# 授权
# Sa-Token
Sa-Token (opens new window) 是一个轻量级 java 权限认证框架