Skip to content

Spring Scurity

介绍

  • Spring Security 是 Spring 家族中的一个安全管理框架;
  • 一般Web应用的需要进行认证和授权;
  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户;
  • 授权:经过认证后判断当前用户是否有权限进行某个操作;

项目搭建

创建Maven工程

  • 添加相应的依赖
java
<!--    父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>
    <dependencies>

<!--        springboot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        SpringSecurity依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--        MybatisPuls-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
<!--        mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
  • 创建启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}

注意:引入依赖后我们在尝试去访问接口就会跳转到一个SpringScurity 的默认登录页面,默认用户是user,密码会输出在控制台,必须登录之后才可以对接口进行访问

认证

登录认证流程

alt text

原理探究

  • SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器

alt text

  • UsernamePasswordAuthenticationFilter(认证):处理在登陆页面填写了用户名密码后的登陆请求

  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException

  • FilterSecurityInterceptor(授权):负责权限校验的过滤器

认证流程

alt text

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息

  • AuthenticationManager接口:定义了认证Authentication的方法

  • UserDetailsService接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法

  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中。

注意: 一般我们会写接口去实现UserDetailsService,并且重写loadUserByUsername方法,这样我们就可以实现通过数据库的查询方式显示用户信息的获取(下面会有重写UserDetailsService类的代码内容)!

具体实现

登录

  • 自定义登录接口
java
@ApiOperation(value = "用户登录")
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = sysLoginManager.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
  • 自定义UserDetailsService
java
@Component
public class UserDetailsManager implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsManager.class);

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysPermissionManager sysPermissionManager;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = sysUserService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户不存在", HttpStatus.ERROR);
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("登录用户已删除", HttpStatus.ERROR);
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("登录用户已停用", HttpStatus.ERROR);
        }
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user, sysPermissionManager.getMenuPermission(user));
    }
}
  • 进行登录进行的逻辑操作
java
/**
 * 用户登录
 *
 * @param username
 * @param password
 * @param code
 * @param uuid
 * @return
 */
public String login(String username, String password, String code, String uuid) {
    // 登录前置校验
    loginPreCheck(username, password);
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
        throw new ServiceException("账号密码错误", HttpStatus.ERROR);
    }
    if (!userDetails.isEnabled()) {
        throw new ServiceException("账号被禁用,请联系管理员", HttpStatus.ERROR);
    }
    //更新security登陆用户对象
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,
            null, userDetails.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    LoginUser loginUser = (LoginUser) userDetails;
    redisCache.setCacheObject("login:" + loginUser.getUserId(), loginUser);
    //生成并返回token
    return jwtTokenUtil.generateToken(userDetails, StringUtils.EMPTY);
}

校验

  • 定义Jwt认证过滤器

①解析token ②解析token获取其中的用户信息 ③ 存入SecurityContextHolder

java
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String autHeader = request.getHeader(tokenHeader);
        //存在token(startsWith() 方法用于检测字符串是否以指定的前缀开始)
        if (null != autHeader && autHeader.startsWith(tokenHead)) {
            //substring(int beginIndex)---beginIndex->起始索引(包括)
            String authToken = autHeader.substring(tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            //token存在用户名但是未登录
            if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
                //登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //验证token是否有效,重新设置用户对象
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}
  • 将过滤器添加到过滤器链中
java
/**
 * 这些请求经过过滤器链,但被允许访问(即放行,不需要认证)
 * HttpSecurity.http().authorizeRequests().permitAll():用于接口放行(如 /login、Swagger 文档)。
 *
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf()
            .disable()
            //基于token,不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(
                    "/login", "/logout", "/captcha", "/ws/**", "/chat/**",
                    "/system/cfg/menu", "/swagger-ui.html", "/doc.html",
                    "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs",
                    "/configuration/ui", "/configuration/security","/wxLogin"
            ).permitAll()
            .anyRequest().authenticated()
            .and()
            //禁用缓存
            .headers()
            .cacheControl();
    //添加Jwt 登录授权过滤器
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    // 配置异常处理器
    http.exceptionHandling()
            // 配置认证失败处理器
            .authenticationEntryPoint(unauthorizedHandler)
            .accessDeniedHandler(accessDeniedHandler);
}

授权

授权的作用

授权就是在用户已经登录的前提下,判断他是否有权限访问某个资源或功能。是保护系统数据安全和功能合理分配的核心机制之一。

授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验

  • 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication
  • 获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限
  • 在项目中只需要把当前登录用户的权限信息也存入Authentication
  • 设置我们的资源所需要的权限即可

限制访问资源所需的权限

  • SpringSecurit心y为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。但是要使用它我们需要先开启相关配置,需要在SecurityConfig类中添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解
java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    ...
}
  • 自定义权限实现类
java
/**
 * 自定义权限实现,ss取自SpringSecurity首字母
 */
@Service("ss")
public class PermissionManager {
    /**
     * 验证用户是否具备某权限
     *
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

    /**
     * 判断是否包含权限
     *
     * @param permissions 权限列表
     * @param permission  权限字符串
     * @return 用户是否具备某权限
     */
    private boolean hasPermissions(Set<String> permissions, String permission) {
        return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}
    • 封装权限信息 在上述自定义UserDetailsService类中会调用sysPermissionManager.getMenuPermission(user)方法,该方法是封装权限信息
java
/**
 * 获取菜单数据权限
 *
 * @param user 用户信息
 * @return 菜单权限信息
 */
public Set<String> getMenuPermission(SysUser user) {
    Set<String> perms = new HashSet<String>();
    // 管理员拥有所有权限
    if (user.isAdmin()) {
        perms.add("*:*:*");
    } else {
        List<SysRole> roles = user.getRoles();
        if (!CollectionUtils.isEmpty(roles)) {
            // 多角色设置permissions属性,以便数据权限匹配权限
            for (SysRole role : roles) {
                Set<String> rolePerms = sysMenuService.selectMenuPermsByRoleId(role.getRoleId());
                role.setPermissions(rolePerms);
                perms.addAll(rolePerms);
            }
        } else {
            perms.addAll(sysMenuService.selectMenuPermsByUserId(user.getUserId()));
        }
    }
    return perms;
}
  • 使用对应的注解:@PreAuthorize
java
/**
 * 获取菜单列表
 */
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@GetMapping("/list")
@ApiOperation(value = "获取菜单列表")
public AjaxResult list(SysMenu menu) {
    List<SysMenu> menus = sysMenuService.selectMenuList(menu, getUserId());
    return AjaxResult.success(menus);
}

自定义失败处理器

在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处 理,要实现这个功能我们需要知道Spring security的异常处理机制.

在SpringSecuritye中,在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用uthenticationEntryPoint对象的方法去进行异常处理。如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity 即可。

  • 认证失败处理器
java
/**
 * 认证失败处理类 返回未认证
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:%s,认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}
  • 授权失败处理器
java
/**
 * 授权失败处理类 返回未授权
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        int code = HttpStatus.FORBIDDEN;
        String msg = StringUtils.format("请求访问:%s,授权失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}
  • 在Spring Security中配置认证授权处理器
java
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf()
            .disable()
            //基于token,不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(
                    "/login", "/logout", "/captcha", "/ws/**", "/chat/**",
                    "/system/cfg/menu", "/swagger-ui.html", "/doc.html",
                    "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs",
                    "/configuration/ui", "/configuration/security","/wxLogin"
            ).permitAll()
            .anyRequest().authenticated()
            .and()
            //禁用缓存
            .headers()
            .cacheControl();
    //添加Jwt 登录授权过滤器
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    // 配置异常处理器
    http.exceptionHandling()
            // 配置认证失败处理器
            .authenticationEntryPoint(unauthorizedHandler)
            .accessDeniedHandler(accessDeniedHandler);
}

跨域处理

  • 自定义跨域处理配置类
java
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
    /**
     * 跨域配置
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址
        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 有效期 1800秒
        config.setMaxAge(1800L);
        // 添加映射路径,拦截一切请求
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        // 返回新的CorsFilter
        return new CorsFilter(source);
    }
}
  • 在Spring Scurity 中添加跨域处理配置类
java
protected void configure(HttpSecurity http) throws Exception {
    http.csrf()
            .disable()
            //基于token,不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(
                    "/login", "/logout", "/captcha", "/ws/**", "/chat/**",
                    "/system/cfg/menu", "/swagger-ui.html", "/doc.html",
                    "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs",
                    "/configuration/ui", "/configuration/security","/wxLogin"
            ).permitAll()
            .anyRequest().authenticated()
            .and()
            //禁用缓存
            .headers()
            .cacheControl();
    //添加Jwt 登录授权过滤器
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter (跨域处理配置类)
    http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    // 配置异常处理器
    http.exceptionHandling()
            // 配置认证失败处理器
            .authenticationEntryPoint(unauthorizedHandler)
            .accessDeniedHandler(accessDeniedHandler);
}

Last updated: