spring security 认证授权详解

spring security简介

Spring Security 是 Spring家族中的一个安全管理框架,它提供了更丰富的功能做认证授权

  • 认证:当前用户有没有权限登录,是否为本系统用户
  • 授权:当前登录的用户有没有操作功能的权限

spring security的搭建

引入依赖



        
            cn.hutool
            hutool-all
            5.5.2
        

        
            com.auth0
            java-jwt
            3.10.3
        

        
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
            org.springframework.boot
            spring-boot-starter-security
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.0
        

        
            com.mysql
            mysql-connector-j
            runtime
        

        
            com.alibaba
            fastjson
            1.2.75
        

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    

 创建数据库表

create table user
(
    password varchar(100) null,
    username varchar(50)  null,
    id       varchar(50)  not null
        primary key
)
    comment '用户表';

spring security 认证授权详解_第1张图片

项目配置如下

server:
  port: 9000
spring:
  datasource:
    url: jdbc:mysql://xxxx:3306/security_db?useSSL=true&serverTimezone=Asia/Shanghai
    username: root
    password: root123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    cluster:
      nodes:
        - 39.106.53.30:6379
        - 39.106.53.30:6380
        - 39.106.53.30:6381
        - 39.106.53.30:6382
        - 39.106.53.30:6383
        - 39.106.53.30:6384
mybatis-plus:
  mapper-locations:  classpath:/mapper/*.xml
  type-aliases-package: com.tech.security.securityservice.model

直接访问接口

没有经过认证的接口都会进入spring security的默认登录界面

spring security 认证授权详解_第2张图片spring security的原理

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

主要的过滤器如下

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器

spring security 认证授权详解_第3张图片

 可以看到,在spring 容器中就有spring security的过滤器链,包括熟悉的过滤器UsernamePasswordAuthenticationFilter、FilterSecurityInterceptor等过滤器

spring security认证

认证流程

spring security 认证授权详解_第4张图片

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中

UsernamePasswordAuthenticationFilter实现类:实现了我们最常用的基于用户名和密码的认证逻辑,封装Authentication对象
DaoAuthenticationProvider实现类:是AuthenticationManager中管理的其中一个Provider,因为是要访问数据库

定义jwt认证工具类

我这里采用的是java-jwt,可能和jj-jwt写法和解析方式有所不同

/**
 * @author sl
 */
@Slf4j
public class JwtTokenUtils {


    /**
     * 额外的数据,越复杂越安全
     */
    private static final String SING_VALUE = "dsdadfsghkjlsdfnmkjsd";

    /**
     * 过期时间
     */
    private static ReactiveRedisOperations redisTemplate;


    public static String getJwtToken(String id){

        String JwtToken = JWT.create()
                .withClaim("userId",id)
                .withExpiresAt(DateUtil.offsetHour(new Date(),1))
                .sign(Algorithm.HMAC256(SING_VALUE));

        return JwtToken;
    }

    public static boolean CheckToken(String token){
        try {
            JWT.require(Algorithm.HMAC256(SING_VALUE)).build().verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    public static String parseTokenInfo(String token){
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING_VALUE)).build().verify(token);

        Claim claim = verify.getClaim("userId");

        return claim.asString();
    }
}

定义userDetails实现类

@Data
public class LoginUser implements UserDetails {

    private String id;

    private String username;

    private String password;

    private String token;

    private String authority;

    @Override
    public Collection getAuthorities() {return null;}

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 定义UserDetailsService实现类

目的是去数据库中查询用户信息,并返回UserDetails对象

/**
 * @author sl
 */
@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username",username);
        User user = userMapper.selectOne(userQueryWrapper);;

        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }

        //TODO 根据用户查询权限信息 添加到LoginUser中

        LoginUser loginUser = new LoginUser();

        loginUser.setId(user.getId());
        loginUser.setUsername(user.getUsername());
        loginUser.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        loginUser.setAuthority("");
        return loginUser;
    }
}

登录以及退出登录

通过认证器进入登录认证流程,退出认证直接删除redis缓存即可

@Service
public class UserServiceImpl implements UserService {


    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 认证器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public LoginUser login(LoginUser user) {

        // 使用security认证
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        LoginUser principal = (LoginUser) authenticate.getPrincipal();
        if (authenticate.isAuthenticated()) {
            String jwtToken = JwtTokenUtils.getJwtToken(principal.getId());
            redisTemplate.opsForValue().set("login_"+principal.getId(), JSON.toJSONString(principal) ,1,TimeUnit.DAYS);
            principal.setToken(jwtToken);
        }

        return principal;
    }

    @Override
    public void logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        redisTemplate.delete("login:"+loginUser.getId());
    }

}

配置自定义认证过滤器进行jwt认证

主要是解析请求头中包含的token信息,并存入SecurityContextHolder上下文中

/**
 * @author Administrator
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        String loginUserString = null;
        try {
            if (!StringUtils.hasText(token)) {
                //放行
                filterChain.doFilter(request, response);
                return;
            }

            if(token !=null){
                String userId= JwtTokenUtils.parseTokenInfo(token);

                loginUserString= redisTemplate.opsForValue().get("login_" + userId);
            }
        }catch (Exception e) {
            throw new RuntimeException("用户未登录");
        }

        LoginUser loginUser = JSON.parseObject(loginUserString, LoginUser.class);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

 将jwt认证过滤器加入过滤器链中

/**
 * @author sl
 */
@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable();
        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated()
                .and()
                .formLogin();
        // 在UsernamePasswordAuthenticationFilter之前添加自定义token前置过滤器
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}

测试认证

直接访问登录接口,传递用户名密码进入认证流程, 返回token

spring security 认证授权详解_第5张图片

携带token访问,认证成功

spring security 认证授权详解_第6张图片

spring security授权 

授权的概念:当前登录的用户有没有操作功能的权限

授权的模型:rbac(Role-Based Access Control)模型

授权方式:注解,配置

rbac授权模型

基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型

spring security 认证授权详解_第7张图片数据库脚本 

共5张表结构,用户表,角色表,权限表,用户角色表,用户权限表

create table perm
(
    id          varchar(60)                not null
        primary key,
    func_name   varchar(64) default 'NULL' not null comment '功能名称',
    path        varchar(200)               null comment '路由地址',
    status      char        default '0'    null comment '功能状态(0正常 1停用)',
    perms       varchar(100)               null comment '权限标识',
    create_time datetime                   null,
    update_time datetime                   null,
    del_flag    int         default 0      null comment '是否删除(0未删除 1已删除)',
    remark      varchar(500)               null comment '备注'
)
    comment '权限表';

INSERT INTO perm (id, func_name, path, status, perms, create_time, update_time, del_flag, remark) VALUES ('dfasdfgfdg', '审批功能', '/audit', '0', 'audit', '2023-10-11 15:20:16', '2023-10-11 15:20:18', 0, '审批功能');
INSERT INTO perm (id, func_name, path, status, perms, create_time, update_time, del_flag, remark) VALUES ('fdsfdsfhgfsjy', '删除功能', '/delete', '0', 'delete', '2023-10-11 11:26:58', '2023-10-11 11:27:02', 0, '删除权限');


create table user
(
    id       varchar(50)  not null
        primary key,
    username varchar(50)  null,
    password varchar(100) null
)
    comment '用户表';

INSERT INTO user (id, username, password) VALUES ('qwertreyt', 'ceshi', 'ceshi');
INSERT INTO user (id, username, password) VALUES ('sdafasf214', 'admin', 'admin');

create table role
(
    id          varchar(60)      not null
        primary key,
    name        varchar(128)     null,
    role_key    varchar(100)     null comment '角色权限字符串',
    status      char default '0' null comment '角色状态(0正常 1停用)',
    del_flag    int  default 0   null comment 'del_flag',
    create_time datetime         null,
    update_time datetime         null,
    remark      varchar(500)     null comment '备注'
)
    comment '角色表';
	
INSERT INTO role (id, name, role_key, status, del_flag, create_time, update_time, remark) VALUES ('qwerqwerer', '管理员', 'amdin', '0', 0, '2023-10-11 11:22:00', '2023-10-11 11:22:09', '管理员角色');
INSERT INTO role (id, name, role_key, status, del_flag, create_time, update_time, remark) VALUES ('uiplpptry', '普通用户', 'ceshi1', '0', 0, '2023-10-11 15:31:39', '2023-10-11 15:31:42', '普通用户');

create table role_perm
(
    role_id varchar(60) not null comment '角色ID',
    perm_id varchar(60) not null comment '菜单id',
    primary key (role_id, perm_id)
);

INSERT INTO role_perm (role_id, perm_id) VALUES ('qwerqwerer', 'dfasdfgfdg');
INSERT INTO role_perm (role_id, perm_id) VALUES ('qwerqwerer', 'fdsfdsfhgfsjy');
INSERT INTO role_perm (role_id, perm_id) VALUES ('uiplpptry', 'fdsfdsfhgfsjy');

create table user_role
(
    user_id varchar(60) not null comment '用户id',
    role_id varchar(60) not null comment '角色id',
    primary key (user_id, role_id)
);

INSERT INTO user_role (user_id, role_id) VALUES ('sdafasf214', 'qwerqwerer');
INSERT INTO user_role (user_id, role_id) VALUES ('qwertreyt', 'uiplpptry');

开启权限配置

@EnableGlobalMethodSecurity(prePostEnabled = true)

/**
 * @author sl
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {

    ....

}

建立mapper通过用户id查询权限




    


将用户权限封装到userDetails对象中 

LoginUser加入权限

package com.tech.security.securityservice.model;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
public class LoginUser implements UserDetails {

    private String id;

    private String username;

    private String password;

    private String token;

    /**
     *存储权限信息
     */
    private List permissions;

    /**
     *存储SpringSecurity所需要的权限信息的集合
     */
    @JSONField(serialize = false)
    private List authorities;

    @Override
    public Collection getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

封装权限到userDetails对象中 

/**
 * @author sl
 */
@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PermMapper permMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username",username);
        User user = userMapper.selectOne(userQueryWrapper);;

        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }

        LoginUser loginUser = new LoginUser();
        loginUser.setId(user.getId());
        loginUser.setUsername(user.getUsername());
        loginUser.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        //根据用户查询权限信息 添加到LoginUser中
        List perms= permMapper.findPermByUserId(user.getId());
        List permsList = perms.stream().map(s -> s.getPerms()).collect(Collectors.toList());
        loginUser.setPermissions(permsList);
        return loginUser;
    }
}

别忘记在自定义认证器中加入权限进入上下文中

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

编写controller加权限注解测试

hasAuthority代表拥有该权限可以访问,还有hasRole等,默认会拼接上ROLE_,在这里不做过多介绍,@PreAuthorize("@myAccess.hasAuthority('audit')"),myAccess为自定义权限校验

/**
 * @author sl
 */
@RestController
public class TestSecurityAccessController {

    /**
     * 测试数据访问接口
     * @return
     */
    @GetMapping("/access")
    @PreAuthorize("hasAuthority('access')")
    public String access(){
        return "access success!!";
    }

    /**
     *  @PreAuthorize("@myAccess.hasAuthority('audit')")
     *  使用自定义权限校验
     * @return
     */
    @GetMapping("/audit")
    @PreAuthorize("@myAccess.hasAuthority('audit')")
    public String audit(){
        return "audit access!!";
    }

    /**
     * 使用spring security权限校验
     * @return
     */
    @GetMapping("/delete")
    @PreAuthorize("hasAuthority('delete')")
    public String delete(){
        return "delete access!!";
    }
}

基于配置的权限配置

        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                // 基于配置的权限处理
                .antMatchers("/audit").hasAuthority("audit")
                .anyRequest().authenticated()
                .and()
                .formLogin();

自定义权限校验

在controller中@PreAuthorize("@myAccess.hasAuthority('audit')") 就使用了自定义的权限校验

/**
 * @author sl
 * 自定义权限校验
 */
@Component("myAccess")
public class MyAccessHandler {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

启动项目开始权限测试 

登录测试账户

当前登录的账户ceshi登录成功,当前测试账户具有删除权限

spring security 认证授权详解_第8张图片

访问无权限的审计功能

当前访问为403,无授权的http状态,无法访问

spring security 认证授权详解_第9张图片

访问删除功能 

访问成功,代表用户具有的删除权限可以访问,授权成功

spring security 认证授权详解_第10张图片

认证授权优化问题 

统一返回结果,自定义异常处理器

当前的认证授权,如果失败了返回的结果不是很友好,我们希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,方便统一结构

spring security的异常处理

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • ​ 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
  • ​所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

工具类WebUtils

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

认证失败处理器

/**
 * @author sl
 * 认证失败返回处理
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
        String json = JSON.toJSONString(result);
        // 处理异常,调用工具类
        WebUtils.renderString(response, json);
    }
    
}

授权失败处理器

/**
 * @author sl
 * 权限校验失败处理
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

将授权以及认证处理器配置给SpringSecurity

/**
 * @author sl
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable();
        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                // 基于配置的权限处理
//                .antMatchers("/audit").hasAuthority("audit")
                .anyRequest().authenticated()
                .and()
                .formLogin();
        // 在UsernamePasswordAuthenticationFilter之前添加自定义token前置过滤器
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 自定义认证失败和权限处理失败处理器
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
        // 允许跨越
        httpSecurity.cors();
        return httpSecurity.build();
    }

}

测试认证失败返回和授权失败返回 

未登录显示认证失败

spring security 认证授权详解_第11张图片

登录之后返回权限不足 

spring security 认证授权详解_第12张图片

项目gitee地址 

https://gitee.com/watcherman/security-service.git

你可能感兴趣的:(spring,family,spring,java,数据库)