Initial understanding of Spring Security

illustrate

Spring Security is a powerful and highly customizable authentication and access control framework. Spring Security is a framework focused on providing authentication and authorization for Java applications. As with all Spring projects, the real power of Spring Security is that it can be easily extended to meet custom needs.
General web applications require authentication and authorization.

  • User authentication (Authentication): To verify whether the current access system is a user of this system, and to confirm which user it is.
  • User authorization (Authorization): After authentication, it is judged whether the current user has permission to perform an operation. In a system, different users have different permissions.

The difference between Spring Security and Shiro

Spring Security is a security management framework in the Spring family. Compared with Shiro, another security framework, it provides richer functions and more community resources than Shiro. Compared with Shiro, it is more troublesome to integrate Spring Security in SSH/SSM, but with Spring Boot, Spring Boot provides an automatic configuration solution for Spring Security, and Spring Security can be used with zero configuration. Therefore, in general, a common combination of security management technology stacks is:

  • SSM+Shiro
  • Spring Boot /Spring Clound +Spring Security

easy to use

Login verification process

insert image description here

Introducing Security

Using Spring Security in a SpringBoot project only needs to introduce dependencies.

<!-- spring security 安全认证 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

Set username and password

Add properties in application.properties:

server.port=8080
#spring.security.user.name=root
#spring.security.user.password=123456
 
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

But adding properties in application.properties means that the username and password of the login system are fixed, which is not recommended. You can use the configuration class, set it in the configuration class, and the configuration class has a higher priority.

Use configuration class

@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;
    
    /**
     * 解决 无法直接注入 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
    {
    
    
        httpSecurity
                // CRSF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 允许匿名访问
                .antMatchers("/login").anonymous()

                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/file/**",
                        "/**/*.js"
                ).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    
    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
    
    
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}
  • @EnableGlobalMethodSecurity: When we want to enable spring method-level security, we only need to use the @EnableGlobalMethodSecurity annotation on any @Configuration instance to achieve this goal.
  • prePostEnabled = true will unlock @PreAuthorize and @PostAuthorize two annotations. As you can see from the name, the @PreAuthorize annotation will be verified before the method is executed, and the @PostAuthorize annotation will be verified after the method is executed.
/**
 * 自定义权限实现,ss取自SpringSecurity首字母
 */
@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 = ",";

    @Autowired
    private TokenService tokenService;

    /**
     * 验证用户是否具备某权限
     *
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
    
    
        if (StringUtils.isEmpty(permission))
        {
    
    
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.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 (StringUtils.isEmpty(permissions))
        {
    
    
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.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 (StringUtils.isEmpty(role))
        {
    
    
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
    
    
            return false;
        }
        for (SysRole sysRole : loginUser.getUser().getRoles())
        {
    
    
            String roleKey = sysRole.getRoleKey();
            if (SUPER_ADMIN.contains(roleKey) || roleKey.contains(StringUtils.trim(role)))
            {
    
    
                return true;
            }
        }
        return false;
    }

    /**
     * 验证用户是否不具备某角色,与 isRole逻辑相反。
     *
     * @param role 角色名称
     * @return 用户是否不具备某角色
     */
    public boolean lacksRole(String role)
    {
    
    
        return hasRole(role) != true;
    }

    /**
     * 验证用户是否具有以下任意一个角色
     *
     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
     * @return 用户是否具有以下任意一个角色
     */
    public boolean hasAnyRoles(String roles)
    {
    
    
        if (StringUtils.isEmpty(roles))
        {
    
    
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.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(StringUtils.trim(permission));
    }
}
    /**
     * 获取用户列表
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
    
    
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }
  • antMatchers(): Multiple String parameters can be passed in to match multiple request APIs. Combined with permitAll(), multiple requests can be set to ignore authentication at the same time.
  • and(): Used to indicate that the previous configuration has ended and the next configuration will start.
    Create a Service file for Security to query user information:
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    
    
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
    
    
    	// 根据账户号查询用户信息
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
    
    
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
    
    
            log.info("登录用户:{} 已被删除.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
    
    
            log.info("登录用户:{} 已被停用.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
    
    
    	// 获取用户权限
        return new LoginUser(user, permissionService.getMenuPermission(user));
    }
}

When the user requests, Security will intercept the request, take out the username field, query the account from the Service, and fill the information into a userDetails object and return it. In this way, Security gets the filled userDetails object, that is, user information including permissions.

WebSecurityConfigurerAdapter.configure(HttpSecurity http) filter rules

Authorization method

There are two ways to authorize Security:

  • WEB authorization (based on request URL), set in configure(HttpSecurity httpSecurity).
  • Method authorization (using annotation authorization on methods)

WEB authorization

@Override
httpSecurity.authorizeRequests() // 对请求进行授权
    .antMatchers("/test1").hasAuthority("p1") // 设置/test1接口需要p1权限
    .antMatchers("/test2").hasAnyRole("admin", "manager") // 设置/test2接口需要admin或manager角色
    .and()
    .headers().frameOptions().disable();

in:

  • hasAuthority("p1") indicates that p1 authority is required
  • hasAnyAuthority("p1", "p2") indicates that either p1 or p2 authority is
    acceptable. The same reason:
  • hasRole("p1") indicates that the p1 role is required
  • hasAnyRole("p1", "p2") indicates that both p1 and p2 roles are acceptable

method authorization

    /**
     * 获取用户列表
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
    
    
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }

order priority

Verification is performed from top to bottom in the order of configuration. If a condition has already been matched, subsequent matches with the same condition will not be executed. That is, if multiple rules are contradictory, only the first rule takes effect. Typically:

// 只有具有权限a的用户才能访问/get/resource
httpSecurity.authorizeRequests()
    .antMatchers("/get/resource").hasAuthority("a")
    .anyRequest().permitAll();

// 所有用户都能访问/get/resource
httpSecurity.authorizeRequests()
    .anyRequest().permitAll()
    .antMatchers("/get/resource").hasAuthority("a");

But if it is a containment relationship, you need to put fine-grained rules in front:

// /get/resource需要权限a,除此之外所有的/get/**都可以随意访问
httpSecurity.authorizeRequests()
    .antMatchers("/get/resource").hasAuthority("a")
    .antMatchers("/get/**").permitAll();

Sign out

httpSecurity.logoutUrl() and httpSecurity.addLogoutHandler() conflict, usually only one of them is used. For the case where token is used and the server does not store it, there is no need to request the server to log out, and the front end simply discards the token.

httpSecurity.logout().logoutUrl(“/logout”).logoutSuccessHandler(logoutSuccessHandler);

/**
 * 自定义退出处理类 返回成功
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    
    
    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     * 
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException
    {
    
    
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
    
    
            String userName = loginUser.getUsername();
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
    }
}

cross domain

httpSecurity.cors()
	.and()
	.csrf().disable();
  • cors() allows cross-origin resource access.
  • csrf().disable() disables cross-domain security verification.

Authentication failure processing class

/**
 * 认证失败处理类 返回未授权
 */
@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("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

Guess you like

Origin blog.csdn.net/qq_41596778/article/details/129854644