spring security的认证和授权功能代码实现

网上看了很多springsecurity的资料,一堆配置,看的还是蒙的很,打算从代码入手。

慢慢深入摸清套路;

登录认证:

首先,新建一个spring-boot项目。写一个api接口作为后续测试;

@RestController
@RequestMapping(value = "/api")
public class ApiController {

    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable String id) {
        return "product id :" + id;
    }

}

既然要使用spring security,pom引入依赖;

        
            org.springframework.boot
            spring-boot-starter-security
        

此时,直接启动项目,访问接口会发现,需要你输入登录账号密码,账号默认是user,密码在后台启动界面有显示。

spring security的认证和授权功能代码实现_第1张图片

输入账号密码后会正常显示接口信息。

也就是说引入spring-security依赖后,默认所有接口会受到保护,请求会跳到默认的login界面;

既然有默认的账号密码,那么这个账号密码应该也是可配置的,实际项目中,用户的账号密码都是数据库中。那么如何按照我们自己的方式来做账号密码登录呢;

先看看官方的例子:

@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                // 定义加密方式,不然启动不了
                .passwordEncoder(new BCryptPasswordEncoder())
                // 设置用户名
                .withUser("admin")
                // 设置密码(密文密码)
                .password(new BCryptPasswordEncoder().encode("123456"))
                // 设置角色,不设置启动不了
                .roles("");
    }
}

他的认证管理器是用内存方式实现的,内存中固定了账号admin,和密码123456;

那么用查数据库中用户信息的方式来认证如何实现呢

Security框架提供了两个接口UserDetails和UserDetailsService。

UserDetails就是获取用户认证相关的信息,需要有个实体类实现相关的方法,例:

public class MyUserBean implements UserDetails {

    private Long id;
    private String name;
    private String address;
    private String username;
    private String password;
    private String roles;

    public MyUserBean(Long id, String name, String address, String username, String password, String roles) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.username = username;
        this.password = password;
        this.roles = roles;
    }

    @Override
    public Collection getAuthorities() {
        String[] authorities = roles.split(",");
        List simpleGrantedAuthorities = new ArrayList<>();
        for (String role : authorities) {
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
        }
        return simpleGrantedAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetailsService接口仅定义了一个方法loadUserByUsername(String username) ,

这个方法由接口的实现类来具体实现,它的作用就是通过用户名username从数据库中查询,并将结果赋值给一个UserDetails的实现类实例

为了方便测试就直接new了一个用户对象,没有从数据库获取,实际应用中可以根据username或者其他唯一字段从库中查找相关用户信息。

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //MyUserBean userBean = mapper.selectByUsername(username);
        MyUserBean userBean = new MyUserBean(9999L,"srf","beijing","666ddd",new BCryptPasswordEncoder().encode("123456"),"ROLE_USER,ROLE_ADMIN");

        if (userBean == null) {
            throw new UsernameNotFoundException("数据库中无此用户!");
        }
        return userBean;
    }
}

然后将认证管理器改为userDetailsService即可

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //加入数据库验证类,下面的语句实际上在验证链中加入了一个DaoAuthenticationProvider
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

此时,用户账号密码是要和loadUserByUsername返回一致即可完成验证;

以上就是认证的过程,实际应用中设计好自己的用户表,按照以上方法可以简单实现一个认证登录了。

用户角色授权:

接下来需要看的是授权的部分,

例如一个后台系统,包含了财务模块,用户模块等等。。。。

对于不同的登录者,他们能看到的模块,或者说界面,能访问的url是不同的。那么如何做到url和登录者用户的权限的匹配呢。

同样需要重写WebSecurityConfigurerAdapter的configure(HttpSecurity http)

先看下源码中默认的方式:

这段的意思就是说所有的请求都需要进行认证到登录界面,所以我们引入spring-security后默认所有的接口都被保护了,需要登录才能访问。

    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }

现在,我们重写该方法,给之前的测试接口加一个权限验证,及/api/product/**这个路径必须是登陆者有ROLE_USER权限才能访问(默认会有ROLE_前缀)。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/product/**").hasRole("USER")
                .anyRequest().authenticated()
                .and().formLogin()
                .and().httpBasic();
    }

这些基本的表达式我来给大家介绍下

表达式 备注
hasRole 用户具备某个角色即可访问资源
hasAnyRole 用户具备多个角色中的任意一个即可访问资源
hasAuthority 类似于 hasRole
hasAnyAuthority 类似于 hasAnyRole
permitAll 统统允许访问
denyAll 统统拒绝访问
isAnonymous 判断是否匿名用户
isAuthenticated 判断是否认证成功
isRememberMe 判断是否通过记住我登录的
isFullyAuthenticated 判断是否用户名/密码登录的
principle 当前用户
authentication 从 SecurityContext 中提取出来的用户对象

这种是表达式控制 URL 路径权限的方法。

 Spring Security 中有四种常见的权限控制方式:

  • 表达式控制 URL 路径权限
  • 表达式控制方法权限
  • 使用过滤注解
  • 动态权限

而我们实际项目中,动态权限应该是用的最多的。以下详情参考大佬文章:https://zhuanlan.zhihu.com/p/47873694

应该会有权限表,有对应url所需的权限关系,并且可配置的。

主要通过重写拦截器和决策器来实现动态的权限控制;

用户访问某资源/xxx时,FilterInvocationSecurityMetadataSource这个类的实现类(本文是MySecurityMetadataSource)会调用getAttributes方法来进行资源匹配。它会读取数据库resource表中的所有记录,对/xxx进行匹配。若匹配成功,则将/xxx对应所需的角色组成一个 Collection返回;匹配不成功则说明/xxx不需要什么额外的访问权限;

流程来到鉴权的决策类AccessDecisionManager的实现类(MyAccessDecisionManager)中,它的decide方法可以决定当前用户是否能够访问资源。decide方法的参数中可以获得当前用户的验证信息、第3步中获得的资源所需角色信息,对这些角色信息进行匹配即可决定鉴权是否通过。当然,你也可以加入自己独特的判断方法,例如只要用户具有ROLE_ADMIN角色就一律放行;

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

        http
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor() {
                    @Override
                    public  O postProcess(O object) {
                        object.setSecurityMetadataSource(mySecurityMetadataSource);
                        object.setAccessDecisionManager(myAccessDecisionManager);
                        return object;
                    }
                });
    }
自定义的元数据源类,用来提供鉴权过程中,访问资源所需的角色:
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    ResourceMapper resourceMapper;
    //本方法返回访问资源所需的角色集合
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        //从object中得到需要访问的资源,即网址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        //从数据库中得到所有资源,以及对应的角色
        List resourceBeans = resourceMapper.selectAllResource();
        for (MyResourceBean resource : resourceBeans) {
            //首先进行地址匹配
            if (antPathMatcher.match(resource.getUrl(), requestUrl)
                    && resource.getRolesArray().length > 0) {
                return SecurityConfig.createList(resource.getRolesArray());
            }
        }
        //匹配不成功返回一个特殊的ROLE_NONE
        return SecurityConfig.createList("ROLE_NONE");
    }

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

    @Override
    public boolean supports(Class clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
本类是鉴权的决策类:
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        //从authentication中获取当前用户具有的角色
        Collection userAuthorities = authentication.getAuthorities();

        //从configAttributes中获取访问资源所需要的角色,它来自MySecurityMetadataSource的getAttributes
        Iterator iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute attribute = iterator.next();
            String role = attribute.getAttribute();

            if ("ROLE_NONE".equals(role)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("用户未登录");
                } else
                    return;
            }
            //逐一进行角色匹配
            for (GrantedAuthority authority : userAuthorities) {
                if (authority.getAuthority().equals("ROLE_ADMIN")) {
                    return; //用户具有ROLE_ADMIN权限,则可以访问所有资源
                }
                if (authority.getAuthority().equals(role)) {
                    return;  //匹配成功就直接返回
                }
            }
        }
        //不能完成匹配
        throw new AccessDeniedException("你没有访问" + object + "的权限!");
    }

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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

 相关权限表sql

-- ----------------------------
--  Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL COMMENT '姓名',
  `address` varchar(64) DEFAULT NULL COMMENT '联系地址',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
  `roles` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
--  Records of `user`
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES ('1', 'Adam', 'beijing', 'adam','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER');
INSERT INTO `user` VALUES ('2', 'SuperMan', 'shanghang', 'super','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_ADMIN');
INSERT INTO `user` VALUES ('3', 'Manager', 'beijing', 'manager','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_MANAGER');
INSERT INTO `user` VALUES ('4', 'User1', 'shanghang', 'user1','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_DEPART1');
INSERT INTO `user` VALUES ('5', 'User2', 'shanghang', 'user2','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_DEPART2');
COMMIT;

-- ----------------------------
--  Table structure for `resource`
-- ----------------------------
DROP TABLE IF EXISTS `resource`;
CREATE TABLE `resource` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) DEFAULT NULL COMMENT '资源',
  `roles` varchar(255) DEFAULT NULL COMMENT '所需角色',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
--  Records of `resource`
-- ----------------------------
BEGIN;
INSERT INTO `resource` VALUES ('1', '/depart1/**', 'ROLE_ADMIN,ROLE_MANAGER,ROLE_DEPART1');
INSERT INTO `resource` VALUES ('2', '/depart2/**', 'ROLE_ADMIN,ROLE_MANAGER,ROLE_DEPART2');
INSERT INTO `resource` VALUES ('3', '/user/**', 'ROLE_ADMIN,ROLE_USER');
INSERT INTO `resource` VALUES ('4', '/admin/**', 'ROLE_ADMIN');
COMMIT;

按照以上便能够简单实现不同角色的授权过程 

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