Spring Security 框架基础

Spring Security 框架基础

应用入门

初始化 spring boot 项目

添加 pom.xml 文件


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>
@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping("/handle01")
    public String handle01() {
        return "你好 spring security";
    }
}

访问 127.0.0.1:8080/demo/handle01 会进入表单页面。

默认 username=user;password 打印在控制台。

认证

httpBasic 认证

传输的用户名和密码使用 base64模式进行加密;是可逆的;不安全。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
    }
}

表单认证

默认的认证方式。UsernamePasswordAuthenticationFilter。默认的登录url : /login POST;参数名:username password 。

开启表单认证:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.formLogin().and().authorizeRequests()
             .antMatchers("/demo/login", "/code/**").permitAll() // 不需要验证的接口
             .anyRequest().authenticated();
    }
}

解决静态资源拦截问题

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) {
        // 排除静态资源被拦截
        web.ignoring().antMatchers("/images/**");
    }
}

基于数据库实现认证功能

操作实体采用 spring-data-jpa。

基本语法可参考:https://blog.csdn.net/qq_43439920/article/details/123165903?spm=1001.2014.3001.5501

开启后,会去数据库验证用户是否合法。控制台不会再打印密码。

@Service
public class PayingUserService implements UserDetailsService {
    @Autowired
    private PayingUserDao payingUserDao;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 基于数据库进行验证
        PayingUser payingUser = new PayingUser();
        payingUser.setUsername(username);
        Example<PayingUser> payingUserExample = Example.of(payingUser);
        Optional<PayingUser> payingUserOptional = payingUserDao.findOne(payingUserExample);
        if (payingUserOptional.isEmpty()) {
            // 用户名没有找到
            System.out.println(username + "没有找到...");
            throw new UsernameNotFoundException(username);
        }
        // 权限集合
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // {noop}:密码的加密方式; noop => 不加密
        return new User(user.getUsername(), "{noop}" + user.getPassword(), authorities);
    }
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private PayingUserService payingUserService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义用户认证
        auth.userDetailsService(payingUserService);
    }
}

用户密码加密认证

// 推荐加密使用:bcrypt 强哈希方法 每次加密的结果都不一样 所以更安全
return new User(user.getUsername(), "{bcrypt}" + user.getPassword(), authorities);
@RequestMapping(value = "/save")
public PayingUser save(String username, String password) {
    // bcrypt 加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encodePassword = bCryptPasswordEncoder.encode(password);
    // 将加密后的数据存入数据库
    PayingUser payingUser = new PayingUser();
    payingUser.setUsername(username);
    payingUser.setPassword(encodePassword);
    payingUser.setStatus(1);
    return payingUserDao.save(payingUser);
}

获取当前用户的三种方式

// 获取当前用户的三种方式
@RequestMapping("/getCurrentUser1")
public UserDetails getCurrentUser1() {
    return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

@RequestMapping("/getCurrentUser2")
public UserDetails getCurrentUser2(Authentication authentication) {
    return (UserDetails) authentication.getPrincipal();
}

@RequestMapping("/getCurrentUser3")
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails) {
    return userDetails;
}

remember-me

方便用户下一次登录的时候,不用再次输入用户名和密码登录。

简单Token

// 记住我功能 默认token失效时间是两周 单位 S
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me")

持久化Token

@Autowired
private DataSource dataSource;

@Bean
// 持久化 Token 支持
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);
    // 自动创建记录 token 的表 下次启动需要注释掉
    // tokenRepository.setCreateTableOnStartup(true);
    return tokenRepository;
}
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository())
# 默认创建的 token 表
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

判断请求是否来自 remember-me

// 判断当前操作是否来自 remember-me
@RequestMapping("/{id}")
public PayingUser getById(@PathVariable Integer id) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
        System.out.println("认证信息来源 remember-me,请重新登录");
        throw new RememberMeAuthenticationException("认证信息来源 remember-me,请重新登录");
    }
    return payingUserDao.findById(id).get();
}

自定义登录成功&失败&退出处理逻辑

@Service
public class AuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败的后续处理...");
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功的后续处理...");
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出登录的后续处理...");
    }
}
@Autowired
private AuthenticationService authenticationService;

.successHandler(authenticationService)
.failureHandler(authenticationService)

图形验证码

生成图形验证码的接口需要不被拦截。

/**
 * 处理生成验证码的请求
 */
@RestController
@RequestMapping("/code")
public class ValidateCodeController {
    public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
    public final static int expireIn = 600;  // 验证码有效时间 60s

    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //获取访问IP
        String remoteAddr = request.getRemoteAddr();
        //生成验证码对象
        ImageCode imageCode = createImageCode();
        //生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
        stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr).set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
        //通过IO流将生成的图片输出到登录页面上
        ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
    }

    /*
        用于生成验证码对象
     */
    private ImageCode createImageCode() {

        int width = 100;    // 验证码图片宽度
        int height = 36;    // 验证码图片长度
        int length = 4;     // 验证码位数
        //创建一个带缓冲区图像对象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //获得在图像上绘图的Graphics对象
        Graphics g = image.getGraphics();

        Random random = new Random();

        //设置颜色、并随机绘制直线
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        //生成随机数 并绘制
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }
        g.dispose();
        return new ImageCode(image, sRand.toString());
    }

    /*
        获取随机演示
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

session 管理

会话超时

application.yml

server:
  servlet:
    session:
      # 设置 session 的过期时间;默认 30 min;spring boot 最低 60 S
      timeout: 60

并发控制

// session 过期后的处理
http.sessionManagement()
    // 无效后跳转页面
    .invalidSessionUrl("/demo/login")
    // 最大会话数量
    .maximumSessions(1)
    // 当达到最大会话数量的时候不允许登录
    .maxSessionsPreventsLogin(true);

集群session

一个服务会至少部署在两台服务器。如果第一次用户访问的是服务器1,第二次访问的是服务器2,用户会多次输入密码。

解决上述情况,可以将 session 存在 redis 中。

pom.xml


<dependency>
    <groupId>org.springframework.sessiongroupId>
    <artifactId>spring-session-data-redisartifactId>
dependency>

application.yml

spring:
  session:
    # session 的存储方式
    store-type: redis

跨域支持

private CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    // 设置允许跨域的站点
    corsConfiguration.addAllowedOrigin("*");
    // 设置允许跨域的http方法
    corsConfiguration.addAllowedMethod("*");
    // 设置允许跨域的请求头
    corsConfiguration.addAllowedHeader("*");
    // 允许带凭证
    corsConfiguration.setAllowCredentials(true);

    // 对所有的url生效
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;
}
// 跨域支持
http.cors().configurationSource(corsConfigurationSource());

授权

url 安全表达式

// 权限集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 设置权限
if (user.getUsername().startsWith("admin")) {
    // 给用户名是 admin 开头的用户 设置管理员权限
    authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
// 设置url 的访问权限 此处是登录成功后能够做的事情
// /demo/** 下的url 需要 admin 权限
// hasRole:指定需要特定的角色的用户允许访问, 会自动在角色前面插入'ROLE_'
http.authorizeRequests().antMatchers("/demo/**").hasRole("ADMIN");

自定义权限不足处理逻辑

@Service
public class DeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        System.out.println("权限不足,请联系管理员");
    }
}
@Autowired
private DeniedHandler deniedHandler;

// 自定义权限不足的信息
http.exceptionHandling().accessDeniedHandler(deniedHandler);

自定义 bean 授权

@Service
public class AuthorizationService {

    // 自定义权限认证
    public boolean checkAuthority(Authentication authentication, HttpServletRequest request) {
        User user = (User) authentication.getPrincipal();
        Collection<GrantedAuthority> authorities = user.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            String authorityName = authority.getAuthority();
            if ("ROLE_ADMIN".equals(authorityName)) {
                System.out.println(user.getUsername() + "有管理员权限");
                return true;
            }
        }
        return false;
    }
}
// 自定义bean 权限认证http.authorizeRequests().antMatchers("/demo/**").access("@authorizationService.checkAuthority(authentication,request)");

method 安全表达式

开启方法级别的注解配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize
@RequestMapping("/findAll")
// 在进入方法前进行验证
@PreAuthorize("hasRole('ADMIN')")
public List<PayingUser> findAll() {
    return payingUserDao.findAll();
}
@RequestMapping("/update/{id}")
@PreAuthorize("#id<10")
public String update(@PathVariable Integer id) {
    return "针对id小于10的用户可访问";
}
@PostAuthorize
@GetMapping("/get/{id}")
// 可以拿到返回值 此处判断的权限是当前登录用户名和返回的用户名相同
@PostAuthorize("returnObject.username == authentication.principal.username")
public PayingUser get(@PathVariable Integer id) {
    return payingUserDao.findById(id).get();
}
@PreFilter
@GetMapping("/delByIds")
// 可以对集合类型的参数进行过滤 将不符合条件的元素剔除
// http://127.0.0.1:8080/demo/delByIds?id=1,2,3,4,5,6,7
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")
public void delByIds(@RequestParam(value = "id") List<Integer> ids) {
    for (Integer id : ids) {
        System.out.println(id); // 2 4 6
    }
}
@PostFilter
@GetMapping("/findAllPayUser")
// 对集合类型的返回值进行过滤
@PostFilter("filterObject.id % 2==0")
public List<PayingUser> findAllPayUser() {
    return payingUserDao.findAll(); // 返回 id = 2,4,6... 的用户
}

RBAC 权限管理

需要五张表。

一个用户可以有多个角色。一个角色可以有多个权限。

  1. 用户表:

    CREATE TABLE `paying_user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(50) COLLATE utf8_bin DEFAULT NULL,
      `password` varchar(100) COLLATE utf8_bin DEFAULT NULL,
      `status` int(1) DEFAULT NULL COMMENT '用户状态1-启用 0-关闭',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=COMPACT;
    
  2. 角色表:

    CREATE TABLE `t_role` (
      `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
      `ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
      `ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
      PRIMARY KEY (`ID`) USING BTREE
    ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
    
  3. 权限表:

    CREATE TABLE `t_permission` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
      `permission_name` varchar(30) DEFAULT NULL COMMENT '权限名称',
      `permission_tag` varchar(30) DEFAULT NULL COMMENT '权限标签',
      `permission_url` varchar(100) DEFAULT NULL COMMENT '权限地址',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
    
  4. 用户-角色关系表:

    CREATE TABLE `t_user_role` (
      `UID` int(11) NOT NULL COMMENT '用户编号',
      `RID` int(11) NOT NULL COMMENT '角色编号',
      PRIMARY KEY (`UID`,`RID`) USING BTREE,
      KEY `FK_Reference_10` (`RID`) USING BTREE,
      CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`),
      CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `paying_user` (`id`)
    ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
    
  5. 角色-权限关系表:

    CREATE TABLE `t_role_permission` (
      `RID` int(11) NOT NULL COMMENT '角色编号',
      `PID` int(11) NOT NULL COMMENT '权限编号',
      PRIMARY KEY (`RID`,`PID`) USING BTREE,
      KEY `FK_Reference_12` (`PID`) USING BTREE,
      CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`),
      CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `t_permission` (`id`)
    ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
    

根据用户 id 查询用户权限的 SQL。

SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,paying_user u 
WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id = ?
// SecurityConfiguration 添加
// 基于数据库的权限认证 
http.authorizeRequests().antMatchers("/demo/**").hasAuthority("user:findAll");
// PayingUserService 添加
List<Permission> permissions = permissionDao.findByUserId(user.getId());
for (Permission permission : permissions) {
    // 根据数据库存到内容设置权限
    authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}

参考git:https://gitee.com/zhangyizhou/learning-spring-security-demo.git

你可能感兴趣的:(java,spring,后端)