springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解

springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第1张图片
cover


上一章我们从源码角度探究了springboot对于帮我们初始化的springsecurity默认配置,这章我们来学习下springsecurity中的基础配置

springboot整合springsecurity系列文章:
springboot整合springsecurity从Hello World到源码解析(一):hello world程序入门
springboot整合springsecurity从Hello World到源码解析(二):springsecurity配置加载解析
springboot整合springsecurity从Hello World到源码解析(三):基础配置详解
springboot整合springsecurity从Hello World到源码解析(四):springsecurity基础架构解析
springboot整合springsecurity从Hello World到源码解析(五):springsecurity+jwt整合restful服务

修改基础配置

  • 上一章我们已经知道,springsecurity中所有配置基本都来源于一个默认的WebSecurityConfigurerAdapter,那我们首先写一个类继承它,放弃springboot帮我们做的默认配置,
    叫SecurityConfig,为了看到更多的配置,我们加上一个注解(其实springboot已经帮我们加上),@EnableWebSecurity(debug = true),修改debug位true,
    然后打开我们的配置文件application.yml,修改spring的log信息为debug,如下:
server:
  port: 8080

spring:
  freemarker:
    enabled: true
    cache: false
    template-loader-path: classpath:/templates/
    suffix: .html

  security:
    user:
      name: user
      password: admin
      roles: user, admin
logging:
  level:
    org.springframework.*: debug

配置详解

  • 打开SecurityConfig,首先明确我们的目的:修改原来的登陆页面,登陆成功后,跳转到我们的hello页面,所以首先添加登陆页面login.html,并且添加视图解析(和第一章一样添加controller同样效果):



    
    login page


This is login page from jsbintask
username:
password:

这里请记住这个表单提交的地址/login,写一个类WebMvcConfig实现WebMvcConfigurer(2.0以前需要继承WebMvcConfigurerAdapter),添加如下配置:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/index").setViewName("login");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    }
}
  • 接着继续回来SecurityConfig,首先覆盖下原方法configure(HttpSecurity http),我们看下原来实现是什么:
protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }

可以看出,默认配置就是所有页面全部被拦截,开启登陆表单验证以及http basic验证,我们继续查看formLogin()方法:

public FormLoginConfigurer formLogin() throws Exception {
        return getOrApply(new FormLoginConfigurer<>());
    }

熟悉的apply方法,上一章已经介绍,这是添加拦截器,FormLoginConfigurer如下:

public FormLoginConfigurer() {
        super(new UsernamePasswordAuthenticationFilter(), null);
        usernameParameter("username");
        passwordParameter("password");
    }

加了一个UsernamePasswordAuthenticationFilter拦截器。接下来,我们修改configure配置如下,值得注意的是,因为现在我们的页面是自己的定义,但是所有页面
都是需要权限的,所以我们必须放行登陆(error页面在BaseErrorController中定义),错误页面:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/index")
            // 和login.html中表单提交的一直必须一样,这样才能让springsecurity帮你处理请求
            .loginProcessingUrl("/login")
            .and()
            .authorizeRequests()
            .antMatchers("/index", "/login", "/error").permitAll()
            .anyRequest()
            .authenticated();
}

接着启动项目,查看控制台,发现多个springsecurity的日志:
o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@60d40ff4, org.springframework.security.web.context.SecurityContextPersistenceFilter@58867cd5, org.springframework.security.web.header.HeaderWriterFilter@2c05ff9d, org.springframework.security.web.csrf.CsrfFilter@44ed0a8f, org.springframework.security.web.authentication.logout.LogoutFilter@70211df5, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4c5228e7, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5a8ab2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@71926a36, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2e5b7fba, org.springframework.security.web.session.SessionManagementFilter@2e1ddc90, org.springframework.security.web.access.ExceptionTranslationFilter@2687725a, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@c29fe36]
看得出这就是我们上一章说的过滤器链了。并且UsernamePasswordAuthenticationFilter也在其中
接下来打开浏览器,直接访问主界面, http://localhost:8080/hello,自动跳转到了我们自定义的登陆页面:

springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第2张图片
security

然后点击提交,发现403错误了,纳尼? 赶紧检查控制台,发现走了一个CrsfFilter,这个filter需要一个参数,防止xss攻击的,但是我们不需要,所以我们禁掉,如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/index")
            .loginProcessingUrl("/login")
            .and()
            .authorizeRequests()
            .antMatchers("/index", "/login", "/error", "/favicon.ico").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf()
            .disable();
}

这回正常了,我们输入错误的用户名,密码,果然,回到了原来的登陆页面,如下:

springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第3张图片
security

并且后面带了一个error的参数,所以如果我们的login页面再做下处理,就能回显用户名密码错误了。然后我们继续输入我们一开始已经配置用户名密码,继续,这回出现了404,
springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第4张图片
security

看地址我们知道它是登陆成功后帮我回到了 http://localhost:8080作为了默认页面,所以我们要加上登陆成功后的页面如下,也就是hello

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/index")
            .loginProcessingUrl("/login")
            .successForwardUrl("/hello")
            .and()
            .authorizeRequests()
            .antMatchers("/index", "/login", "/error", "/favicon.ico").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf()
            .disable();
}

继续登陆,果然,成功后帮我们重定向到了hello页面:


springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第5张图片
security

自定义数据查询

经过上面的配置,我们一开始的目的达到了,自定义登陆页面,并且登陆成功后跳转到主界面,但是现在还有个问题是,我们的用户名密码是配置配置文件中的,这样肯定不行,
因为我们一般都是使用数据库的。接下来就是我们自定义数据源了。

内存中的数据源

上一篇博客我们已经通过源码分析了springboot在启动的时候帮我们初始化了一个在内存中的UserDetailService,如下:

springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第6张图片
security

那我们现在先来覆盖掉这个,回到先前的SecurityConfig,并且继承方法 configure(AuthenticationManagerBuilder auth)
在自定义UserDetailsService的时候,发现它要求返回一个UserDetails,所以我们需要继承这个类来返回自己的实体类User,因为我们这里使用内存中的实现,可以直接用它提供的工具方法:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(new InMemoryUserDetailsManager(
            User.builder().username("jsbintask1").password("{noop}123456").authorities("jsbintask1").build(),
            User.builder().username("jsbintask2").password("{noop}123456").authorities("jsbintask2").build()
    ));
}

值得注意的是,如果我们以这种方式定义密码的时候,要在密码前面加上{noop}这个前缀或者配置一个密码加密器的bean,否则验证会出错。另外还有一点就是一定要添加roles或者authorities,
否则springsecurity不予通过。现在我们重新登陆,并且使用一开始配置文件中的用户名密码,发现此时已经不行了。 再用我们的新用户名密码,通过!


springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解_第7张图片
security

到这里,我们的自定义内存中的数据源就定义好了,接下来我们换成数据库中的形式。

db形式的数据源

  • 因为要使用数据库,那我们就选用spring-data jpa去操作数据库,首先引入依赖:

    org.springframework.boot
    spring-boot-starter-data-jpa


    mysql
    mysql-connector-java

接下来就是配置mysql数据源和hibernate的属性了:

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: jason
    url:  jdbc:mysql://localhost:3306/springsecurity_demos?useSSL=false

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

初始化sql脚本如下:

CREATE DATABASE springsecurity_demos;
USE springsecurity_demos;

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
                       `id` int(11) NOT NULL AUTO_INCREMENT,
                       `role_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
                       PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'user', 'this is a user role.');
INSERT INTO `role` VALUES (2, 'admin', 'this is a admin role.');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
                       `id` int(11) NOT NULL AUTO_INCREMENT,
                       `username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `age` int(11) NULL DEFAULT NULL,
                       `address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
                       `role_id` int(11) NOT NULL,
                       PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'jsbintask', '$2a$10$WfUOGFJzAkPeRU3ZK2q6v.FNFcgIBrhixFNQ/htTKx71RK/OBMYaC', 22, 'China, Wuhan', 0);

SET FOREIGN_KEY_CHECKS = 1;

我们的数据库一共有两张表,user表和role表,此处为了简单,我们的user和role设为1对多关系,user表中添加role_id,然后编写User, Role实体类,UserRepository, RoleRepository:

@Table
@Entity
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;
    private String password;

    private Integer age;

    private String address;

    private Integer roleId;
}

@Repository
public interface UserRepository extends JpaRepository {
    User findByUsername(String username);
}

@Entity
@Table
@Data
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String roleName;
    private String description;
}

@Repository
public interface RoleRepository extends JpaRepository {
}

好了,接下就是要与sprinsecurity结合了,在此之前,还有一点需要我们明白,springsecurity中对于用户的表示有自己的实体类相对应,它就是UserDetails,所以我们编写一个AuthUser实现这个接口:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthUser implements UserDetails {
    private String username;
    private String password;
    private List roles;

    @Override
    public Collection getAuthorities() {
        return roles.isEmpty() ? Collections.EMPTY_LIST :
                (roles.parallelStream().map(role -> new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()));
    }

    @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;
    }
}

其中关于账号过期,enable全部设置为通过,接下来就是编写CustomUserDetailsService,返回我们自己的AuthUser:

@Service
@Primary
public class CustomUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserRepository userRepository;

    @Resource
    private RoleRepository roleRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException("user: " + username + " is not found.");
        }

        return new AuthUser(user.getUsername(), user.getPassword(), roleRepository.findAllById(Collections.singletonList(user.getRoleId())));
    }
}

注意,此处如果没有找到用户,要抛出 UsernameNotFoundException,然后springsecurity会处理。
到此,其实我们的UserDetails已经注入了Spring中, what? 不是还只定义了吗,其实我们加上@Service和@Primary之后,springsecurity就能自己检测到这个bean,然后作为自己的UserDetailsService,
当然,如果我们像上面一样使用覆盖方法的方式同样可行。 但是接下来还有差一步,我们上面已经说到了,再springsecurity中一定要对密码进行处理,比如我们上面就是不加密,在前面加上{noop},这里我们不这里处理,
我们配置一个密码加密器:

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

同样,springsecurity也能检测到,这样定义就ok了,那这样就有另一个问题了,我们的密码是加密过了,所以我们的数据库初始化一个加密过的密码才行,所以我们再使用这个加密器生成一个密码“123456”:

public static void main(String[] args) {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    System.out.println(bCryptPasswordEncoder.encode("123456"));
}

然后将密码放入数据库。然后同样去登陆,输入数据库中用户名,密码,同样帮我们转发了主页面hello,至此,我们的db中的数据源定义也同样通过了!

源码地址(麻烦点个star哦):https://github.com/jsbintask22/spring-security-demos.git

结束语:

本次我们手把手操作并且总结了springsecurity的一般用法,并且提出了很多值得注意的点。 那么接下来我们就站在一个高一点角度来分析一下springsecurity的架构吧。

本文原创地址:https://jsbintask.cn/2019/01/11/springsecurity-basicconfig/,未经允许,禁止转载。

你可能感兴趣的:(springboot整合springsecurity从Hello World到源码解析(三):-基础配置详解)