springboot-security 功能体验

一、security简介

  • Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架,其分为两个部分:Authentication(认证)和 Authorization(授权)

二、功能体验

Authentication(认证)

1. 默认登录验证

  • 创建springboot工程,springboot版本 2.2.2.RELEASE,security版本5.2.1
  • 引入security 依赖包
    	 <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
    
  • 启动springboot 工程,security 会生成一个默认的用户信息,用户名 user
    • 控制台打印默认密码:
      在这里插入图片描述
    • 浏览器浏览服务任意地址会进入登录验证:
      在这里插入图片描述

2. 默认内存登录验证

  • security 默认提供两种认证方式in-memory和jdbc,下边例子为 in-memory方式

  • 添加几个页面

    • home.html作为主页

      <!DOCTYPE html>
      <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
      <head>
         <title>Spring Security Example</title>
      </head>
      <body>
      <h1>Welcome!</h1>
      <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
      </body>
      </html>
      
    • login.html 登录验证页面

      <!DOCTYPE html>
      <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
          xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
      <head>
        <title>Spring Security Example </title>
      </head>
      <body>
      <div th:if="${param.error}">
        Invalid username and password.
      </div>
      <div th:if="${param.logout}">
        You have been logged out.
      </div>
      <form th:action="@{/login}" method="post">
        <div><label> User Name : <input type="text" name="username"/> </label></div>
        <div><label> Password: <input type="password" name="password"/> </label></div>
        <div><input type="submit" value="Sign In"/></div>
      </form>
      </body>
      </html>
      
    • hello.html 登录成功后的页

      <!DOCTYPE html>
      <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
            xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
      <head>
          <title>Hello World!</title>
      </head>
      <body>
      <h1>Hello world!</h1>
      </body>
      </html>
      
  • 配置视图解析器

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/home").setViewName("home");
            registry.addViewController("/").setViewName("home");
            registry.addViewController("/hello").setViewName("hello");
            registry.addViewController("/login").setViewName("login");
        }
    }
    
  • 配置安全策略

    • @EnableWebSecurity 启用security 安全策略支持,覆盖WebSecurityConfigurerAdapter类配置一些安全细节

    • configure(HttpSecurity)方法定义哪些URL路径应该受到保护,哪些不应该受到保护,如:本例:‘/’,’/home’路径不受安全校验。

    • userDetailsService()方法用于用户信息的构建。

      @Configuration
      @EnableWebSecurity
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.authorizeRequests()
                  .antMatchers("/", "/home").permitAll()
                  .anyRequest().authenticated()
                  .and()
                  .formLogin()
                  .loginPage("/login")
                  .permitAll()
                  .and()
                  .logout()
                  .permitAll();
          }
          
          @Bean
          @Override
          public UserDetailsService userDetailsService() {
              UserDetails user =
                      User.withDefaultPasswordEncoder()
                              .username("user")
                              .password("password")
                              .roles("USER")
                              .build();
              return new InMemoryUserDetailsManager(user);
          }
      }
      
  • 运行结果

    • 进入home页
      在这里插入图片描述
    • 进入login页
      在这里插入图片描述
    • 输入自定义账号进入 hello页
      在这里插入图片描述

3. 登出

  • 修改hello 页面,指定默认登出路径 /logout

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
          xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out"/>
    </form>
    </body>
    </html>
    
  • 展示结果

    • hello 页
      在这里插入图片描述
    • 点击登出,跳转到登录页,注意浏览器地址: /login?logout
      在这里插入图片描述

4. 自定义登录验证

  • 自定义实现参考

    https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/#tech-userdetailsservice

  • security 的核心接口UserDetails 作为用户的信息载体,如要自定义用户表单需对其进行实现

  • UserDetailsS​​ervice 接口作为UserDetails 的提供方,在自定义时也需对其重写

  1. pom文件引入orm框架,本次选择mybatis

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
    
  2. 配置数据源

    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/security02?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=password
    
  3. 用户表

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
      `id` bigint(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) NOT NULL,
      `password` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    
  4. user 定义实现UserDetails 接口,其默认提供5中方法,包括账号是否过期、是否锁定等信息,默认是false ,将其修改为true

    public class User implements UserDetails,Serializable {
        private Integer id;
        private String username;
        private String password;
        
        @Override
        public Collection<? extends GrantedAuthority> 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;
        }
        //get、set 方法略
        }
    
  5. 定义userMapper

    @Mapper
    public interface UserMapper {
        @Select("select * from user where username = #{username} ")
        User getUserByUsername(String username);
    }
    
  6. 自定义service 实现UserDetailsService,其只有一个方法loadUserByUsername,将我们自定义的user 响应即可

    	@Service
    	public class MyUserDetailsService implements UserDetailsService {
    	    @Autowired
    	    private UserMapper userMapper;
    	    @Override
    	    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	        User user = userMapper.getUserByUsername(username);
    	        return user;
    	    }
    	}
    
  7. WebSecurityConfig 配置修改引入自定义的service

    @Autowired
        private MyUserDetailsService userDetailsService;
        
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
  8. 用户密码加密建一个工具类用于生成密码,讲其生成的密码存入库中

    public class PasswordEncoderUtil {
        public static void main(String[] args) {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            String encoded = encoder.encode("123456");
            System.out.println(encoded);
         
        }
    }
    
  9. 写一个接口修改hello页面响应

    @Controller
    public class LoginController {
        @RequestMapping(value = "/hello")
        public ModelAndView hello(){
            Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String username = null;
            if (principal instanceof UserDetails) {
                 username = ((UserDetails)principal).getUsername();
            } else {
                 username = principal.toString();
            }
            System.out.println(username);
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("username1",username);
            modelAndView.setViewName("hello");
            return modelAndView;
        }
    }
    
  10. hello 页面修改

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
          xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
    <h1 th:text = "'你好,'+ ${username1}"></h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out"/>
    </form>
    </body>
    </html>
    

11 结果显示
在这里插入图片描述

5. 记住我功能

  • 是服务记住用户登录一段时间功能,使用户免于短时内重复登录。security提供了两种实现方式。一种将用户信息加密存入cookie中,用户发送请求时用于验证。另一种方式更安全一些,系将用户信息加密生成token值存入数据中,再配合cookie存一个随机串用于取值校验。

    参考:https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/#ns-remember-me

  • 本文选用第二种。security提供了一张表结构用于存用户token信息
    DROP TABLE IF EXISTS `persistent_logins`;
    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;
    
  • security配置引用token配置
    @Autowired
        private DataSource dataSource;
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setDataSource(dataSource);
            jdbcTokenRepository.setCreateTableOnStartup(false);//启动创建表,自己创建了这里设置false
            return jdbcTokenRepository;
        }
    
  • configure 配置rememberMe,userDetailsService实现需要引用,过期设置1小时
    protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .rememberMe().tokenRepository(persistentTokenRepository())
                 .tokenValiditySeconds(3600).userDetailsService(userDetailsService).key("security_key");
            http.exceptionHandling().accessDeniedHandler(new CustomizeAccessDeniedHandler());
        }
    
  • login 页面修改
     <div><label><input name="remember-me" type="checkbox" />remember me</label></div>
    
  • 结果,数据库中生成一条数据,用户在1小时内免登陆
    在这里插入图片描述

Authorization(授权)

1. 角色验证

  • 添加角色表和 用户角色关系表

    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role` (
      `id` bigint(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role` (
      `user_id` bigint(11) NOT NULL,
      `role_id` bigint(11) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  • 建立对应表的mapper

    @Mapper
    public interface RoleMapper {
        @Select(" SELECT t1.* FROM role t1,user_role t2 WHERE t1.id = t2.role_id and t2.user_id = #{userId}")
        List<Role> getRolesByUserId(Integer userId);
    }
    
  • WebSecurityConfig(WebSecurityConfigurerAdapter接口的实现) 类上添加注解@EnableGlobalMethodSecurity(prePostEnabled=true) 开启security 全局注解应用

  • 添加一验证接口,添加 @PreAuthorize(“hasRole(‘role02’)”) 注解用于验证用户拥有的角色

        @RequestMapping("/role")
        @ResponseBody
        @PreAuthorize("hasRole('role02')")
        public String hashRole(){
            return "该用户拥有角色 role01";
        }
    

    hasRole 系security 提供的用于验证角色的功能,其模式添加一前缀“ROLE_”,所以在数据库中保存的角色名需要按其格式来。

    源码中:private String defaultRolePrefix = “ROLE_”;
    文档中: Returns true if the current principal has the specified role. By default if the supplied role does not start with ‘ROLE_’ it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

  • 添加角色表实体,其实现了接口GrantedAuthority,GrantedAuthority用于存security
    的权限,此处权限包括 角色 和 其他权限,默认区分是通过 ROLE_ 前缀,此处知识参考

    https://www.cnblogs.com/longfurcat/p/9417422.html

    public class Role implements Serializable, GrantedAuthority {
        private Integer id;
        private String name;
        public Integer getId() {
            return id;
        }
        
        public void setId(Integer id) {
            this.id = id;
        }
        
        public String getName() {
            return name;
        }
        
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String getAuthority() {
            return name;
        }
    }
    
  • 修改User实体,用于给用户赋角色,修改getAuthorities()方法

    public class User implements UserDetails,Serializable {
    
        private List<Role> authorities;
        
        public void setAuthorities(List<Role> authorities) {
            this.authorities = authorities;
        }
        
        @Override
        public List<Role> getAuthorities() {
            return authorities;
        }
      //其他代码略,可参考源码
    }
    
  • 修改自定义类MyUserDetailsService,用于取用户的角色信息

      @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userMapper.getUserByUsername(username);
            if (user != null){
                user.setAuthorities(roleMapper.getRolesByUserId(user.getId()));
            }
            return user;
        }
    
  • hello.html 添加一行

    <a href="/role" >检查角色权限</a>
    
  • 验证通过
    用户拥有该角色

  • 如果验证未通过,控制台打印

    org.springframework.security.access.AccessDeniedException: 不允许访问
    AccessDeniedException

    页面默认会跳转到error.html.

  • 如果想自定义角色验证异常处理,对AccessDeniedHandler 实现

    public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
        
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/javascript;charset=utf-8");        
            response.getWriter().print("当前登录人,无该角色权限");
        }
    }
    

    WebSecurityConfig配置自定义的权限配置

    http.exceptionHandling().accessDeniedHandler(new CustomizeAccessDeniedHandler());
    
  • 效果
    在这里插入图片描述

2. 权限验证

  • security 通过PermissionEvaluator接口提供权限校验,如果想用其hasPermission做权限校验,需自定对PermissionEvaluator接口进行实现,框架默认的实现一致返回false
    在这里插入图片描述
  • 自定义权限表,本例权限表与角色为多对多关系,故建立关系表,permission表中permission 为权限 如 增删改查 信息,target 为资源信息,可以是url,目标等任何信息。
    	DROP TABLE IF EXISTS `permission`;
    	CREATE TABLE `permission` (
    	  `id` int(11) NOT NULL AUTO_INCREMENT,
    	  `permission` varchar(255) DEFAULT NULL COMMENT '权限',
    	  `name` varchar(255) DEFAULT NULL COMMENT '权限名',
    	  `target` varchar(255) DEFAULT NULL COMMENT '权限目标',
    	  PRIMARY KEY (`id`)
    	) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    	
    	DROP TABLE IF EXISTS `role_permission`;
    	CREATE TABLE `role_permission` (
    	  `role_id` int(11) NOT NULL,
    	  `permission_id` int(11) DEFAULT NULL
    	) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  • entity
    public class Permission implements Serializable {
        private int id;
        
        private String permission;
        
        private String name;
        
        private String target;
        //set get 略
    }
    
  • mapper. 根据userId 查询权限集
    @Mapper
    public interface PermissionMapper {
        
        @Select("SELECT t5.* from `user` t1 INNER JOIN user_role t2 " +
                        " ON t1.id = t2.user_id INNER JOIN role t3 on " +
                        " t2.role_id = t3.id INNER JOIN role_permission t4 ON " +
                        " t4.role_id = t3.id INNER JOIN permission t5 on " +
                        " t5.id = t4.permission_id WHERE t1.id = #{userId}")
        List<Permission> queryPermissionByUserId(Integer userId);
    }
    
  • 自定义PermissionEvaluator,重写hasPermission方法,查询登录用户的权限集与接口访问信息匹配,有匹配上的返回true,结束校验。
    @Component
    public class CustomPermissionEvaluator implements PermissionEvaluator {
        @Autowired
        private PermissionMapper permissionMapper;
        @Override
        public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
            if (authentication == null || targetDomainObject == null || permission == null){
                return false;
            }
            User user = (User) authentication.getPrincipal();
            List<Permission> permissions = permissionMapper.queryPermissionByUserId(user.getId());
            if (!CollectionUtils.isEmpty(permissions)){
                for (Permission permission1 : permissions) {
                    if (permission1.getTarget().equals(targetDomainObject) && permission1.getPermission().equals(permission)){
                        return true;
                    }
                }
            }
            return false;
        }
        
        @Override
        public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
            return false;
        }
    }
    
  • 将自定义的类 注入到WebSecurityConfig中配置,此处官方建议用DefaultMethodSecurityExpressionHandler 处理,表示方法级别权限校验,配上之后报:No ServletContext set error.异常,暂不知原因。先用DefaultWebSecurityExpressionHandler 处理。
    @Bean
        public DefaultWebSecurityExpressionHandler methodSecurityExpressionHandler() {
            DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
            handler.setPermissionEvaluator(new CustomPermissionEvaluator());
            return handler;
        }
    
  • controller添加一接口
        @RequestMapping(value = "/permission")
        @ResponseBody
        @PreAuthorize("hasPermission('user','query')")
        public String hasPermission1(){
            return "该用户对资源‘user’有查的权限";
        }
    
  • 如果权限通过
    在这里插入图片描述
  • 如果权限不通过,依然被AccessDeniedException异常处理所捕获,稍微改造一下
    public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
        
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/javascript;charset=utf-8");
            Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String username = null;
            if (principal instanceof UserDetails){
                username = ((UserDetails)principal).getUsername();
            }
            String servletPath = request.getServletPath();
            response.getWriter().print("当前登录人"+username+",无"+servletPath+"该路径权限");
        }
    }
    
  • 效果
    在这里插入图片描述

三 源码地址

https://github.com/hanpengbo/SpringBoot/tree/master/springboot-security

四 本文参考知识地址

https://spring.io/guides/gs/securing-web/
https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/
https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/

五 未完待续

  • 刚接触security ,写文章的目的是为了 督促自己了解知识尽量全面系统,未完待续。
  • security 学的不舒服,不得劲。我喜欢条条大路通罗马的感觉,学这个有种过独木桥的感觉,慢慢来吧,么得办法
  • 过两天看看shiro对照一下吧
发布了6 篇原创文章 · 获赞 2 · 访问量 238

猜你喜欢

转载自blog.csdn.net/han451375136/article/details/103984924