spring boot整合spring security实现基于rbac的权限控制

1.spring security基于rbac权限控制

1.1 引入依赖

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mayday</groupId>
            <artifactId>mayday_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

1.2配置文件

server:
  port: 9003
spring:
  application:
    name: mayday-auth
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: admin
  redis:
    host: 127.0.0.1
eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:9001/eureka/
  instance:
    prefer-ip-address: true

mybatis:
  type-aliases-package: com.mayday.auth.pojo
  configuration:
    map-underscore-to-camel-case: true

rsa:
  key:
    pubKeyFile: D:/document/key/key_rsa.pub
    priKeyFile: D:/document/key/key_rsa
  allowPaths:
    - /auth/login/**
    - /auth/code/**

1.3 创建pojo类

@Data
@Entity
@Table(name = "tb_user")
public class User implements UserDetails {
    @Id
    @KeySql(useGeneratedKeys = true)
    private Long id;
    private String username;
    private String password;
    private Boolean status;
    private String phone;
    @Transient
    private String checkCode;
    private List<Role> roles;

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(Role::getPermissions).findAny().get();

    }


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

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

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

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }
}
@Data
@Entity
@Table(name = "tb_role")
public class Role {
    @Id
    @KeySql(useGeneratedKeys = true)
    private Long id;
    @Column(name = "role_name")
    private String roleName;
    @Column(name = "role_desc")
    private String roleDesc;
    private List<Permission> permissions;
}
@Data
@Entity
@Table(name = "tb_permission")
public class Permission implements GrantedAuthority {
    @Id
    @KeySql(useGeneratedKeys = true)
    private Long id;
    @Column(name = "permission_name")
    private String permissionName;
    @Column(name = "permission_desc")
    private String permissionDesc;

    @Override
    @JsonIgnore
    public String getAuthority() {
        return permissionName;
    }
}

1.4 编写dao和service层

DAO层

user接口
public interface UserMapper extends Mapper<User> {
}

role接口
public interface RoleMapper extends Mapper<Role> {
    @Select("select r.id,r.role_name,r.role_desc from tb_role r left join tb_user_role ur on  r.id = ur.role_id where ur.user_id = #{userId}")
    List<Role> selectAllByUserId(Long userId);
}

permission接口
public interface PermissionMapper extends Mapper<Permission> {
    @Select("select p.id,p.permission_name,p.permission_desc from tb_permission p left join tb_role_permission rp on  p.id = rp.permission_id where rp.role_id = #{roleId}")
    List<Permission> selectAllByRoleId(Long roleId);
}

service 层

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PermissionMapper permissionMapper;
    @Autowired
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        user.setUsername(username);
        User currentUser = userMapper.selectOne(user);
        if (currentUser == null) {
            return null;
        }
        //设置用户的角色
        List<Role> roles = roleMapper.selectAllByUserId(currentUser.getId());
        if (!CollectionUtils.isEmpty(roles)) {
            for (Role role : roles) {
                //查询每个角色对应的权限
                List<Permission> permissions = permissionMapper.selectAllByRoleId(role.getId());
                if (!CollectionUtils.isEmpty(permissions)) {
                    role.setPermissions(permissions);
                }
            }
            currentUser.setRoles(roles);
        }
        return currentUser;
    }
}

1.5 controller层

@RestController
@RequestMapping("/auth")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RsaKeyProperties rsaKeyProperties;

    private static final String REDIS_PREFIX = "mayday:login:code";

    private static final int OUT_TIME = 30;
    private static final String TOKEN_PREFIX = "Bearer ";

    /**
     * 生成验证码
     *
     * @param response
     * @param request
     * @throws IOException
     */
    @GetMapping("/code")
    public void captcha(HttpServletResponse response, HttpServletRequest request) throws IOException {
        response.setHeader("Cache-Control", "no-store, no-cache");
        response.setContentType("image/jpeg");
        // 生成图片验证码
        BufferedImage image = CaptchaUtil.createImage();
        // 生成文字验证码
        String randomText = CaptchaUtil.drawRandomText(image);
        // 保存到验证码到 redis 有效期两分钟
        redisTemplate.opsForValue().set(REDIS_PREFIX, randomText.toLowerCase(), 1, TimeUnit.MINUTES);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image, "jpeg", out);
    }

    /**
     * 登录
     *
     * @param user
     * @return
     */
    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        //检查验证码是否正确
        String checkCode = user.getCheckCode();
        String redisCode = redisTemplate.opsForValue().get(REDIS_PREFIX);
        if (StringUtils.isEmpty(checkCode) || !redisCode.equals(checkCode)) {
            return new Result(false, StatusCode.CHECK_CODE_NOT_EXIT.getCode(), StatusCode.CHECK_CODE_NOT_EXIT.getMsg());
        }
        //验证登录
        User currentUser = (User) userService.loadUserByUsername(user.getUsername());
        if (currentUser == null) {
            return new Result(false, StatusCode.USERNAME_NOT_FOUND.getCode(), StatusCode.USERNAME_NOT_FOUND.getMsg());
        }
        //检验密码是否输入正确
        if (new BCryptPasswordEncoder().matches(user.getPassword(), currentUser.getPassword())) {
            return new Result(false, StatusCode.LOGIN_ERROR.getCode(), StatusCode.LOGIN_ERROR.getMsg());
        }
        String token = TOKEN_PREFIX + JwtUtils.generateTokenExploreInMinutes(currentUser, rsaKeyProperties.getPrivateKey(), OUT_TIME);
        return new Result(true, StatusCode.OK.getCode(), StatusCode.OK.getMsg(), token);
    }

    /**
     * 权限测试接口
     * @return
     */
    @GetMapping("test")
    @PreAuthorize("hasAnyAuthority('product_list')")
    public String test() {
        return "test success";
    }
}

2 配置

2.1 创建配置类

@Data
@ConfigurationProperties("rsa")
public class AllowPathsProperties {
    private List<String> allowPaths;
}
@Data
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {

    private String pubKeyFile;
    private String priKeyFile;


    public PublicKey publicKey;
    private PrivateKey privateKey;

    /**
     * 在创建对象之后执行的方法
     *
     * @throws Exception
     */
    @PostConstruct
    public void createRsaKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyFile);
        privateKey = RsaUtils.getPrivateKey(priKeyFile);
    }
}

创建Security配置类

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;
    @Autowired
    private RsaKeyProperties rsaKeyProperties;
    @Autowired
    private AllowPathsProperties allowPathsProperties;


    /**
     * 解决无法直接注入AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 装载BCrypt密码编码器 密码加密
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置UserDetailsService   使用BCrypt进行密码的hash
        auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                // 由于使用的是JWT,我们这里不需要csrf
                .csrf().disable()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 图标 要允许匿名访问
                .antMatchers(allowPathsProperties.getAllowPaths().stream().toArray(String[]::new)).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable()
                .and().addFilter(new LoginFilter(super.authenticationManager(), rsaKeyProperties))
                //基于token,不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        ;
    }
}

2.2创建过滤器

public class LoginFilter extends BasicAuthenticationFilter {

    private RsaKeyProperties rsaKeyProperties;

    public LoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            //如果携带错误的token,则给用户提示请登录!
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map resultMap = new HashMap();
            resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
            resultMap.put("msg", "请登录!");
            out.write(new ObjectMapper().writeValueAsString(resultMap));
            out.flush();
            out.close();
        } else {
            //如果携带了正确格式的token要先得到token
            String token = header.replace("Bearer ", "");
            //验证token是否正确
            Payload<User> payload = JwtUtils.genInfoFromToken(token, rsaKeyProperties.getPublicKey(), User.class);
            User user = payload.getUserInfo();
            if (user != null) {
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            }
        }
    }
}

3启动类

@SpringBootApplication
@MapperScan("com.mayday.auth.dao")
@EnableDiscoveryClient
@EnableConfigurationProperties({RsaKeyProperties.class, AllowPathsProperties.class})
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class);
    }
}

4自定义异常处理

/**
 * 认证失败处理类 返回401
 *
 * @author lihaodong
 */
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        //如果携带错误的token,则给用户提示请登录!
        PrintWriter out = response.getWriter();
        Result result = new Result(false, StatusCode.UN_AUTHORIZATION);
        out.write(new ObjectMapper().writeValueAsString(result));
        out.flush();
        out.close();
    }
}
/**
 * 权限不足认证
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //如果携带错误的token,则给用户提示请登录!
        PrintWriter out = httpServletResponse.getWriter();
        Result result = new Result(false, StatusCode.ACCESS_ERROR);
        out.write(new ObjectMapper().writeValueAsString(result));
        out.flush();
        out.close();
    }
}

在webSecurityConfig配置类上加上

 @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;
     @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                // 由于使用的是JWT,我们这里不需要csrf
                .csrf().disable()
                //认证失败处理类
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)
                }
}

5 测试

Alt
将生成的token填入测试接口中
Alt
Alt
Alt

发布了15 篇原创文章 · 获赞 83 · 访问量 8270

猜你喜欢

转载自blog.csdn.net/weixin_42339552/article/details/103462310