springboot学习笔记|第四篇:Spring Boot Security

1.入门案例

  • 创建Spring Boot项目

  • 引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>2.1.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.1.7.RELEASE</version>
    </dependency>
    
  • 书写请求接口

    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello Stream";
        }
    }
    
  • 启动并访问接口,会自动跳转至如下界面,该界面为Spring Security提供
    在这里插入图片描述
    登录用户名默认为user,密码启动时会打印在控制台,如下图所示
    在这里插入图片描述

  • 配置用户名和密码properties.properties

spring.security.user.name=admin
spring.security.user.password=admin123

上述配置完成后,启动时,登录名及密码为adminadmin123

2.基于内存认证

  • 实现简单认证功能

平时使用时可以自己继承自WebSecurityConfigurerAdapter,从而实现自定义配置

public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

 @Bean
 PasswordEncoder passwordEncoder() {
   return NoOpPasswordEncoder.getInstance();
 }
 
@Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.inMemoryAuthentication()
       .withUser("admin")
       .password("admin123")
       .roles("ADMIN", "USER")
       .and()
       .withUser("zhangsan")
       .password("123")
       .roles("USER");
 }

说明:
(1) passwordEncoder: 在Spring Security 5.x中引入了多种密码加密方式,开发者必须指定一种,如果不指定则访问时会报如下错误
在这里插入图片描述
(2) 重写configure(auth):该方法主要定义用户及其它权限,主要说明如下表

api用途
withUser定义用户名
password定义用户密码
roles定义用户权限(admin:表示管理员,user:表示普通用户)
  • 自定义资源认证保护
 @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .hasRole("ADMIN")
        .antMatchers("/user/**")
        .access("hasAnyRole('ADMIN','USER')")
        .antMatchers("/db/**")
        .access("hasRole('ADMIN') and hasRole('DBA')")
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .loginProcessingUrl("/login")
        .permitAll()
        .and()
        .csrf()
        .disable();
  }

api说明:

api说明
antMatchers匹配请求路径
hasRole表示访问路径需要的权 限
hasAnyRole表示任意一个权限即可
access表示多个权限组合
anyRequest表示除了前面定义的URL模式之外,用户必须登录后访问
authenticated表示开启单点登录
formLogin表示提供表单登录页面
loginProcessingUrl表示登录请求接口
permitAll表示和登录相关接口都不需要认证即可访问
csrf() disable()表示关闭csrf

上述配置完成,书写测试接口如下:

@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello Stream";
  }

  @GetMapping("/admin/hello")
  public String admin() {
    return "hello admin!";
  }

  @GetMapping("/user/hello")
  public String user() {
    return "hello user!";
  }

  @GetMapping("/db/hello")
  public String dba() {
    return "hello dba!";
  }
}
  • 登录表单配置,主要配置成功及失败处理逻辑,在上述配置中继续如下配置
		.loginPage("/login_page")
		.usernameParameter("name")
       .passwordParameter("password")
       .successHandler((httpServletRequest, resp, auth) -> {
         Object principal = auth.getPrincipal();
         resp.setContentType("application/json;charset=utf-8");
         PrintWriter out = resp.getWriter();
         resp.setStatus(200);
         Map<String, Object> map = new HashMap<>();
         map.put("status", 200);
         map.put("msg", principal);
         ObjectMapper om = new ObjectMapper();
         out.write(om.writeValueAsString(map));
         out.flush();
         out.close();
       })
       .failureHandler((req, resp, e) -> {
         resp.setContentType("application/json;charset=utf-8");
         PrintWriter out = resp.getWriter();
         resp.setStatus(401);
         Map<String, Object> map = new HashMap<>();
         map.put("status", 401);
         if (e instanceof LockedException) {
           map.put("msg", "账户被锁定,登录失败!");
         } else if (e instanceof BadCredentialsException) {
           map.put("msg", "账户名或密码输入错误,登录失败!");
         } else if (e instanceof DisabledException) {
           map.put("msg", "账户被禁用,登录失败!");
         } else if (e instanceof AccountExpiredException) {
           map.put("msg", "账户过期,登录失败!");
         } else if (e instanceof CredentialsExpiredException) {
           map.put("msg", "密码过期,登录失败!");
         } else {
           map.put("msg", "登录失败!");
         }
         ObjectMapper obj = new ObjectMapper();
         out.write(obj.writeValueAsString(map));
         out.flush();
         out.close();
       })

api说明:

api用途
loginPage表示自定义登录页面 地址
usernameParameter请求认证参数名
passwordParameter请求认证参数名
successHandler表示成功逻辑
failureHandler表示失败逻辑
  • 注销登录配置
		.and()
        .logout()
        .logoutUrl("/logout")
        .clearAuthentication(true)
        .invalidateHttpSession(true)
        .addLogoutHandler((req, resp, auth) -> {
        })
        .logoutSuccessHandler(
            (request, response, authentication) -> 	response.sendRedirect("/login_page"))
  
api说明
logout开启注销登录配置
logoutUrl注销url
clearAuthentication表示清除认证信息,默认true
invalidateHttpSession表示是否使Session失效,默认true
addLogoutHandler可完成一些清除工作
logoutSuccessHandler退除成功后的操作
  • 密码加密

在spring Boot中配置密码只需要修改上下配置的PasswordEncoder即可

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

平时使用时,在注册用户时对密码进行加密,主要通过如下方式

 @GetMapping("/save/user")
  public int reg(String username,String password){
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    String encodePassword = encoder.encode(password);
    return userService.saveUser(username, encodePassword);
  }

3.基于数据库的认证

  • 设计数据库
    (1) user表
create table user
(
   id       int auto_increment
       primary key,
   username varchar(32)  null,
   password varchar(255) null,
   enabled  tinyint(1)   null,
   locked   tinyint(1)   null
);

INSERT INTO study.user (id, username, password, enabled, locked) VALUES (1, 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);
INSERT INTO study.user (id, username, password, enabled, locked) VALUES (2, 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);
INSERT INTO study.user (id, username, password, enabled, locked) VALUES (3, 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);

(2) role表

create table role
(
   id     int auto_increment
       primary key,
   name   varchar(32) null,
   nameZh varchar(32) null
);

INSERT INTO study.role (id, name, nameZh) VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO study.role (id, name, nameZh) VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO study.role (id, name, nameZh) VALUES (3, 'ROLE_user', '用户');

(3) user_role表

create table user_role
(
   id  int auto_increment
       primary key,
   uid int null,
   rid int null
);

INSERT INTO study.user_role (id, uid, rid) VALUES (1, 1, 1);
INSERT INTO study.user_role (id, uid, rid) VALUES (2, 1, 2);
INSERT INTO study.user_role (id, uid, rid) VALUES (3, 2, 2);
INSERT INTO study.user_role (id, uid, rid) VALUES (4, 3, 3);
  • 引入数据库相关依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.16</version>
     <scope>runtime</scope>
   </dependency>
   <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
   <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid</artifactId>
     <version>1.1.19</version>
   </dependency>
   <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.18.8</version>
   </dependency>
  • 书写配置application.properties
数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/study

# 配置mapper文件及对应实体位置 
mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.study.secturity.dao.entity
  • 创建实体
@Data
public class MyUserDetail implements UserDetails {

 private User user;

 private List<Role> roles;

 //获取当前用户对象所具有的角色信息
 @Override
 public Collection<? extends GrantedAuthority> getAuthorities() {
   List<SimpleGrantedAuthority> authorities = new ArrayList<>();
   for (Role role : roles) {
     authorities.add(new SimpleGrantedAuthority(role.getName()));
   }
   return authorities;
 }

 //获取当前用户对角的密码
 @Override
 public String getPassword() {
   return user.getPassword();
 }

 //获取当前用户对象的用户名
 @Override
 public String getUsername() {
   return user.getUsername();
 }

 //当前账户是否过期
 @Override
 public boolean isAccountNonExpired() {
   return true;
 }

 //当前账户是否锁定
 @Override
 public boolean isAccountNonLocked() {
   return Objects.equals(user.getLocked(), (byte) 0) ? true : false;
 }

 //当前账户是否过期
 @Override
 public boolean isCredentialsNonExpired() {
   return true;
 }

 //当前账户是否可用
 @Override
 public boolean isEnabled() {
   return Objects.equals(user.getEnabled(), (byte) 1) ? true : false;
 }
}

代码说明

方法作用
getAuthorities获取当前用户对象所具有的角色信息
getPassword获取当前用户对象密码
getUsername获取当前用户对象用户名
isAccountNonExpired当前账户是否过期
isAccountNonLocked当前账户是否锁定
isCredentialsNonExpired当前密码是否过期
isEnabled当前账户是否可用
  • 创建userService
@Service
public class UserService implements UserDetailsService{

  @Resource
  private UserMapper userMapper;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userMapper.loadUserByUsername(username);
    if(Objects.isNull(user)){
      throw new UsernameNotFoundException("账户不存在!");
    }
    MyUserDetail myUserDetail = new MyUserDetail();
    myUserDetail.setUser(user);
    myUserDetail.setRoles(userMapper.getUserRolesByUid(user.getId()));
    return myUserDetail;
  }
  }
  • 配置SpringSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Resource
  UserService userService;

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

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .hasRole("admin")
        .antMatchers("/db/**")
        .hasRole("dba")
        .antMatchers("/user/**")
        .hasRole("user")
        .and()
        .formLogin()
        .loginProcessingUrl("/login")
        .permitAll()
        .and()
        .csrf()
        .disable();
  }

上述验证流程说明:
(1) 启动程序时,加载配置文件webSecurityConfig,确定密码加密方式,获取用户信息userService
(2) 当用户请求配置的访问路径时,则会先根据传入的登录用户,查询数据库权限,如果该权限有访问该路径权限则可以正常访问,如果没有权限,则会报出403错误码

4.动态配置权限

  • 数据库设计
    (1)创建menu主要存放请求路径
create table menu
(
    id       int auto_increment
        primary key,
    parttern varchar(64) null
);

INSERT INTO study.menu (id, parttern) VALUES (1, '/db/**');
INSERT INTO study.menu (id, parttern) VALUES (2, '/admin/**');
INSERT INTO study.menu (id, parttern) VALUES (3, '/user/**');

(2)创建menu_role主要存放请求路径与角色关系

create table menu_role
(
    id  int auto_increment
        primary key,
    mid int null,
    rid int null
);

INSERT INTO study.menu_role (id, mid, rid) VALUES (1, 1, 1);
INSERT INTO study.menu_role (id, mid, rid) VALUES (2, 2, 2);
INSERT INTO study.menu_role (id, mid, rid) VALUES (3, 3, 3);
  • 自定义FilterInvocationSecurityMetadataSource
@Component
public class MyMetadataSource implements FilterInvocationSecurityMetadataSource {

  AntPathMatcher antPathMatcher = new AntPathMatcher();

  @Resource
  MenuMapper menuMapper;

  @Override
  public Collection<ConfigAttribute> getAttributes(Object obj) throws IllegalArgumentException {
    String requestUrl = ((FilterInvocation) obj).getRequestUrl();
    List<Menu> allMenus = menuMapper.getAllMenus();
    for (Menu menu : allMenus) {
      if(antPathMatcher.match(menu.getPattern(),requestUrl)){
        List<Role> roles = menu.getRoles();
        String[] roleArr = new String[roles.size()];
        for (int i = 0; i < roleArr.length; i++) {
          roleArr[i] = roles.get(i).getName();
        }
        return SecurityConfig.createList(roleArr);
      }
    }
    return SecurityConfig.createList("ROLE_LOGIN");
  }

  @Override
  public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
  }

  @Override
  public boolean supports(Class<?> clazz) {
    return FilterInvocation.class.isAssignableFrom(clazz);
  }
}

主要实现读取权限与请求路径关系配置,保存至一个集合中

  • 自定义 AccessDecisionManager
@Component
public class MyAccessDecisionMagger implements AccessDecisionManager {

  @Override
  public void decide(Authentication auth, Object object,
      Collection<ConfigAttribute> ca)
      throws AccessDeniedException, InsufficientAuthenticationException {
    Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
    for (ConfigAttribute configAttribute : ca) {
      if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
          && auth instanceof UsernamePasswordAuthenticationToken) {
        return;
      }
      for (GrantedAuthority authority : authorities) {
        if (configAttribute.getAttribute().equals(authority.getAuthority())) {
          return;
        }
      }
    }
    throw new AccessDeniedException("权限不足");
  }

  @Override
  public boolean supports(ConfigAttribute attribute) {
    return true;
  }

  @Override
  public boolean supports(Class<?> clazz) {
    return true;
  }
}
  • 配置webSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Resource
  UserService userService;

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

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
          @Override
          public <O extends FilterSecurityInterceptor> O postProcess(O object) {
            object.setSecurityMetadataSource(cfisms());
            object.setAccessDecisionManager(cadm());
            return object;
          }
        })
        .and()
        .formLogin()
        .loginProcessingUrl("/login")
        .permitAll()
        .and()
        .csrf()
        .disable();
  }

  @Bean
  MyAccessDecisionMagger cadm() {
    return new MyAccessDecisionMagger();
  }

  @Bean
  MyMetadataSource cfisms() {
    return new MyMetadataSource();
  }
}

其中还包含mapper,和实体如下
mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.secturity.dao.mapper.MenuMapper">
  <select
    id="getAllMenus"
    resultMap="BaseResultMap">
   select  m.*,r.id as rid,r.name as rname,r.nameZh as rnamZh from menu m left join
   menu_role  mr on m.id = mr.mid left join role r on mr.rid = r.id
  </select>
  <resultMap id="BaseResultMap" type="com.study.secturity.dao.entity.Menu">
    <id property="id" column="id"/>
    <result property="pattern" column="parttern"/>
    <collection property="roles" ofType="com.study.secturity.dao.entity.Role">
      <id property="id" column="rid"/>
      <result property="name" column="rname"/>
      <result property="nameZh" column="rnamZh"/>
    </collection>
  </resultMap>

</mapper>

实体类

@Data
public class Menu{

  /**主键*/
  private int id;

  /**请求路径*/
  private String pattern;

  private List<Role> roles;

}

经过上边配置,则可实现数据库配置,从而实现动态配置权限。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值