6.springBoot(2.X)+Security实现动态的权限控制

根据我前几篇关于学习SSM+springSecurity一系列博客明白了security的基本操作,接下来我们实现springBoot+Security实现动态的权限控制

  • 本博客数据库中的测试数据是之前博客中的数据
  • 由于springBoot+security的操作与SSM+springSecurity的基本原理是一致的,这里就简单的进行介绍一下项目的构成,以及一些注意事项

1. 首先构建一个springBoot+security+mybatis的项目
6.springBoot(2.X)+Security实现动态的权限控制_第1张图片

  • 增加fastjson、druid等倚赖,最终pom文件:
 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hydata</groupId>
    <artifactId>springsecurity_springboot_01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecurity_springboot_01</name>
    <description>springSecurity01 for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.18</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 在application.yml中编写数据源相关信息:
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123
    type: com.alibaba.druid.pool.DruidDataSource
mybatis:
  type-aliases-package: com.hydata.springsecurity_springboot_01.entity
  mapper-locations: classpath:mybatis/mapper/*Dao.xml
  config-location: classpath:mybatis/mybatis.xml
  • 使用easyCode插件生成controller、service、dao层以及entity、Mapper文件
    6.springBoot(2.X)+Security实现动态的权限控制_第2张图片
    注意:这里我们需要对easyCode生成的SysUser进行改造;对其实现UserDetails接口,并对接口中的方法进行实现
@Data
public class SysUser implements Serializable , UserDetails {
    private static final long serialVersionUID = 573810770728994662L;
    
    private Integer id;
    
    private String username;
    
    private String realname;
    
    private String password;
    
    private Object createdate;
    
    private Object lastlogintime;
    
    private boolean enabled;
    
    private boolean accountnonexpired;
    
    private boolean accountnonlocked;
    
    private boolean credentialsnonexpired;

    private Collection<? extends GrantedAuthority> authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.accountnonexpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.accountnonlocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.accountnonexpired;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}
  • 以及在启动文件上加上@MapperScan(“xxxxxxxxxxxx”)注解

6.springBoot(2.X)+Security实现动态的权限控制_第3张图片

  • 在测试代码中编写测试用例,测试基本配置是否正确
@SpringBootTest
class SpringsecuritySpringboot01ApplicationTests {

    @Autowired
    private SysUserDao sysUserDao;

    @Test
    void queryById() {
        SysUser sysUser = sysUserDao.queryById(1);
        System.out.println(sysUser.toString());
    }
}
  • 测试成功
    在这里插入图片描述
    2. 对项目进行与security整合前的准备工作

  • 前期准备工作(页面,controller等)
    页面结构:
    6.springBoot(2.X)+Security实现动态的权限控制_第4张图片
    这里需要注意的login.html其他页面的代码就不粘贴了:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="/login" method="post">
    用户名:<input type="text" name="username"/><br/>
    密  码:<input type="password" name="password"/><br/>
    验证码:<input type="text" name="imageCode"><img src="/getVerifyCode"/><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

controller层的结构:
6.springBoot(2.X)+Security实现动态的权限控制_第5张图片
MainController :

@Controller
public class MainController {



    @RequestMapping("index")
    public String index(){
        return "index";
    }


    @RequestMapping(value = "login",method = RequestMethod.GET)
    public String login(){
        return "login";
    }

    @RequestMapping("accessDeined")
    public String accessDeined(){
        return "accessDeined";
    }
}

ImageContrller :

@Controller
@Slf4j
public class ImageContrller {

    /* 获取验证码图片*/

    @RequestMapping("/getVerifyCode")
    public void getVerificationCode(HttpServletResponse response, HttpServletRequest request) {
        OutputStream os = null;
        try {

            int width = 200;

            int height = 69;

            BufferedImage verifyImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            //生成对应宽高的初始图片

            String randomText = VerifyCode.drawRandomText(width, height, verifyImg);

            //单独的一个类方法,出于代码复用考虑,进行了封装。

            //功能是生成验证码字符并加上噪点,干扰线,返回值为验证码字符

            request.getSession().setAttribute("verifyCode", randomText);

            response.setContentType("image/png");//必须设置响应内容类型为图片,否则前台不识别

            os = response.getOutputStream(); //获取文件输出流

            ImageIO.write(verifyImg, "png", os);//输出图片流

            os.flush();


        } catch (IOException e) {

            log.error(e.getMessage());

            e.printStackTrace();

        } finally {
            try {
                if (os != null) {
                    os.close();//关闭流
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

VerifyCode :

public class VerifyCode {

    public static  String drawRandomText(int width, int height, BufferedImage verifyImg) {

        Graphics2D graphics = (Graphics2D)verifyImg.getGraphics();

        graphics.setColor(Color.WHITE);//设置画笔颜色-验证码背景色

        graphics.fillRect(0, 0, width, height);//填充背景

        graphics.setFont(new Font("微软雅黑", Font.BOLD, 40));

        //数字和字母的组合

        String baseNumLetter= "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";

        StringBuffer sBuffer = new StringBuffer();

        int x = 10;  //旋转原点的 x 坐标

        String ch = "";

        Random random = new Random();

        for(int i = 0;i < 4;i++){

            graphics.setColor(getRandomColor());

            //设置字体旋转角度

            int degree = random.nextInt() % 30;  //角度小于30度

            int dot = random.nextInt(baseNumLetter.length());

            ch = baseNumLetter.charAt(dot) + "";

            sBuffer.append(ch);

            //正向旋转

            graphics.rotate(degree * Math.PI / 180, x, 45);

            graphics.drawString(ch, x, 45);

            //反向旋转

            graphics.rotate(-degree * Math.PI / 180, x, 45);

            x += 48;

        }

        //画干扰线

        for (int i = 0; i <6; i++) {

            // 设置随机颜色

            graphics.setColor(getRandomColor());

            // 随机画线

            graphics.drawLine(random.nextInt(width), random.nextInt(height),

                    random.nextInt(width), random.nextInt(height));

        }

        //添加噪点

        for(int i=0;i<30;i++){

            int x1 = random.nextInt(width);

            int y1 = random.nextInt(height);

            graphics.setColor(getRandomColor());

            graphics.fillRect(x1, y1, 2,2);

        }

        return sBuffer.toString();

    }

    /**
     * 随机取色
     */

    private static Color getRandomColor() {

        Random ran = new Random();

        Color color = new Color(ran.nextInt(256),

                ran.nextInt(256), ran.nextInt(256));

        return color;

    }

}

3. 接下来正式进行整合

  • 新建一个类进行继承WebSecurityConfigurerAdapter(抽象类),这个类的目的就是实现SSM+SpringSecurity中springSecurity.xml的作用;然后重写其中的方法,其中常用的有两个:
  • protected void configure(AuthenticationManagerBuilder auth):看名字就大概能猜出这个方法是针对配置文件中的 ,用于指定验证类,加密方式等
  • protected void configure(HttpSecurity http):看名字就大概能猜出这个方法是针对配置文件中的 ,用于指定登录模式(FormLogin、HttpBasic)、登录页面、资源访问的权限、放行资源、给自定义过滤器指定执行顺序、退出、记住我等等
    6.springBoot(2.X)+Security实现动态的权限控制_第6张图片
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private SysUserServiceImpl sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private MyOnecPerFilter myOnecPerFilter;

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

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

       http.authorizeRequests()
               .antMatchers("/login","/index","/getVerifyCode").permitAll()
//               注意:hasRole,hasAnyRole方法会自动加上前缀"ROLE_";而hasAnyAuthority不会
               .antMatchers("/sysUser/add").hasRole("ADD")
               .antMatchers("/sysUser/list").hasAnyAuthority("ROLE_LIST")
               .antMatchers("/sysUser/update").hasAnyAuthority("ROLE_UPDATE")
               .antMatchers("/sysUser/delete").hasAnyAuthority("ROLE_DELETE")
               .antMatchers("/**").authenticated()
               .and()
               .formLogin().loginPage("/login")
//               认证成功后跳转的页面,与successHandler不共存
//               .defaultSuccessUrl("/index")
               .successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler)
               .and()
               .csrf().disable();
        http.addFilterBefore(myOnecPerFilter,UsernamePasswordAuthenticationFilter.class);

    }


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

    }


}

《这里不知道为什么打开login页面的访问映射路径非得是login,若访问映射路径改成其他名字(例如MyLogin)并在SecurityConfiguration中对其(例如MyLogin)放行且指定登录页面是修改后的名字(例如MyLogin),那样的话,登录成功后仍然会访问登录页面而不会显示认证成功处理器返回的json,这里有哪位小伙伴知道的话可以指点一下我,谢谢!》
从代码中可以看出我们指定了加密方式BCryptPasswordEncoder,自定义认证逻辑SysUserService(该类实现UserDetailsService接口,重写loadUserByUsername方法,并在方法中定义认证逻辑),认证成功后的处理器myAuthenticationSuccessHandler,认证失败后的处理器myAuthenticationFailureHandler,以及我们将自定义的过滤器myOnecPerFilter放在了UsernamePasswordAuthenticationFilter前边执行。

  • 接下来我们就针对以上自定义的逻辑进行展示

认证失败后的处理器myAuthenticationFailureHandler:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        Map<String,Object> map=new HashMap<>(2);
        map.put("success",false);
        if(e.getMessage()!=null){
            if(e.getMessage().contains("UserDetailsService returned null")){
                map.put("mes","用户名不存在");
            }else if(e.getMessage().contains("Bad credentials")){
                map.put("mes","账号或密码输入错误");
            }else{
                map.put("mes",e.getMessage());
            }
        }
        String result = JSONObject.toJSONString(map);
        httpServletResponse.setContentType("text/json;charset=UTF-8");
        httpServletResponse.getWriter().write(result);
    }
}

认证成功后的处理器myAuthenticationSuccessHandler:

@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        chain.doFilter(request,response);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        Map<String,Object> map=new HashMap<>(2);
        map.put("success",true);
        map.put("mes","登陆成功");
        String result = JSONObject.toJSONString(map);
        httpServletResponse.setContentType("text/json;charset=UTF-8");
        httpServletResponse.getWriter().write(result);
    }


}

自定义的过滤器myOnecPerFilter:

@Component
@Slf4j
public class MyOnecPerFilter extends OncePerRequestFilter {

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        log.debug("URL:" + requestURI + "----------METHOD:" + method);
        if (requestURI.contains("login") && method.equalsIgnoreCase("POST")) {
            try {
                String imageCode = request.getParameter("imageCode");
                if (StringUtils.isEmpty(imageCode)) {
                    throw new MyAuthenticationException("请输入验证码");
                }
                String verifyCode = (String) request.getSession().getAttribute("verifyCode");
                if (!imageCode.equalsIgnoreCase(verifyCode)) {
                    throw new MyAuthenticationException("请输入正确的验证码");
                }
            } catch (AuthenticationException e) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        chain.doFilter(request, response);
    }
}

自定义的过滤器中使用到的异常:

public class MyAuthenticationException extends AuthenticationException {
    public MyAuthenticationException(String msg, Throwable t) {
        super(msg, t);
    }

    public MyAuthenticationException(String msg) {
        super(msg);
    }
}

SysUserServiceImpl 中自定义认证逻辑(该类实现UserDetailsService接口,重写loadUserByUsername方法,并在方法中定义认证逻辑):

@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService, UserDetailsService {
    @Resource
    private SysUserDao sysUserDao;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        SysUser sysUser = sysUserDao.queryByUserName(userName);
        List<GrantedAuthority> list=new ArrayList();
        if(sysUser!=null){
            List<SysPermission> permTags = sysUserDao.queryPermTagsByUserName(userName);
            for (SysPermission permTag : permTags) {
                list.add(new SimpleGrantedAuthority(permTag.getPermtag()));
            }
            sysUser.setAuthorities(list);
        }

        return sysUser;
    }
}

接下来sysUserDao定义两个查询方法,并编写sql

SysUser queryByUserName(String userName);

List<SysPermission> queryPermTagsByUserName(String userName);
<!--查询单个-->
    <select id="queryByUserName" resultMap="SysUserMap" parameterType="string">
        select
          id, username, realname, password, createDate, lastLoginTime, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
        from security.sys_user
        where username = #{username}
    </select>
    <!--根据用户名查询所有权限-->
    <select id="queryPermTagsByUserName" resultType="sysPermission" parameterType="string">
              SELECT sys_permission.id,sys_permission.permName,sys_permission.permTag
	      from (select id ,username from sys_user where username=#{userName}) user
		  join sys_user_role user_role on user.id=user_role.user_id
		  join sys_role_permission role_permission on user_role.role_id=role_permission.role_id
		  join sys_permission on sys_permission.id=role_permission.perm_id

    </select>

最后进行访问localhost:8080/login进行登录测试即可

4. 方法及注意

  • 权限方法说明

方法含义

access(String)				参数为SpEL,如果返回为true则允许访问)

anonymous()					允许匿名访问

authorizerequests()			限定通过签名的请求

anyRequest()				限定任意的请求

hasAnyrole(String…)			将访问权限赋予多个角色(角色会自动加入前缀“ROLE”)

hasRole(String)				将访问权限赋予一个角色(角色会自动加入前缀“ROLE”)

permitAll()					无条件允许访问

and()						连接词,并取消之前限定前提规则

httpbasico()				启用浏览器的HTTP基础验证

formLogin()					启用 Spring Security默认的登录页面

not()						对其他方法的访问采取求反

fullyAuthenticated()		如果是完整验证(并非 Remember--me),则允许访问

dentAll()					无条件不允许任何访问

haslpAddress(String)		如果是给定的IP地址则允许访问

rememberme() 				用户通过 Remember-me功能验证就允许访问

hasAuthority(String) 		如果是给定的角色就允许访问(不会自动加入前缀“ROLE”)

hasAnyAuthority(String)  	如果是给定的角色中的任意一个就允许访问(不会自动加入前缀“ROLE”)
  • access(String)中可以使用spring EL表达式,返回true这允许访问:
    例如:
http.authorizeRequests()
				//允许所有人(无论是否认证)访问login、index、getVerifyCode
                .antMatchers("/login", "/index", "/getVerifyCode").permitAll()
                //允许角色是ROLE_ADD或者ROLE_ADMIN并且是完整登录(非记住我登录)进行访问/sysUser/add
                .antMatchers("/sysUser/add").access("hasAuthority('ROLE_ADD') || hasRole('ADMIN') && isFullyAuthenticated()")

方法含义

authentication()		用户认证对象

denyAll()				拒绝任何访问

hasanyrole((String…)	当前用户是否存在参数中列明的对象属性

hasRole(String)			当前用户是否存在角色

haslpAddress(String)	是否请求来自指定的IP

isAnonymous()			是否匿名访问

isAuthenticated()		是否用户通过认证签名 isFullyAuthenticatedO是否用户是完整验证,即非“记住我”(Remember me认证)功能通过的认

isRememberMe()			是否是通过“记住我”功能通过的验证

permitAll()				无条件允许任何访问

principal()				用户的principal对象
  • Pbkdf2passwordEncoder加密方式
    这种加密方式是在服务端存储密钥,通过密钥对用户输入的密码进行加密、解密
    在application.yml中添加:
 system:
 	user:
 		password:
 			secret:xxxx(密钥)

在实例化加密方式采用

  @Value("${system.user.password.secret}")
    private String secret;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new Pbkdf2PasswordEncoder(this.secret);

    }

你可能感兴趣的:(springSecurity)