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

版权声明:本文为博主原创文章(https://jsbintask.cn),未经博主允许不得转载。 https://blog.csdn.net/Iperishing/article/details/86505120

cover

cover

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

修改基础配置

  • 上一章我们已经知道,springsecurity中所有配置基本都来源于一个默认的WebSecurityConfigurerAdapter,那我们首先写一个类继承它,放弃springboot帮我们做的默认配置,
    叫SecurityConfig,为了看到更多的配置,我们加上一个注解(其实springboot已经帮我们加上),@EnableWebSecurity(debug = true),修改debug位true,
    然后打开我们的配置文件application.yml,修改spring的log信息为debug,如下:
 1server:
 2  port: 8080
 3
 4spring:
 5  freemarker:
 6    enabled: true
 7    cache: false
 8    template-loader-path: classpath:/templates/
 9    suffix: .html
10
11  security:
12    user:
13      name: user
14      password: admin
15      roles: user, admin
16logging:
17  level:
18    org.springframework.*: debug

配置详解

  • 打开SecurityConfig,首先明确我们的目的:修改原来的登陆页面,登陆成功后,跳转到我们的hello页面,所以首先添加登陆页面login.html,并且添加视图解析(和第一章一样添加controller同样效果):
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4    <meta charset="UTF-8">
 5    <title>login page</title>
 6</head>
 7<body>
 8This is login page from jsbintask
 9<form action="/login" method="post">
10    username: <input name="username" /><br/>
11    password: <input name="password" /><br/>
12    <button type="submit">submit</button>
13</form>
14</body>
15</html>

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

 1@Configuration
 2@EnableWebMvc
 3public class WebMvcConfig implements WebMvcConfigurer {
 4    @Override
 5    public void addViewControllers(ViewControllerRegistry registry) {
 6        registry.addViewController("/index").setViewName("login");
 7    }
 8
 9    @Override
10    public void addResourceHandlers(ResourceHandlerRegistry registry) {
11    }
12}
  • 接着继续回来SecurityConfig,首先覆盖下原方法configure(HttpSecurity http),我们看下原来实现是什么:
 1protected void configure(HttpSecurity http) throws Exception {
 2        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
 3
 4        http
 5            .authorizeRequests()
 6                .anyRequest().authenticated()
 7                .and()
 8            .formLogin().and()
 9            .httpBasic();
10    }

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

1public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
2        return getOrApply(new FormLoginConfigurer<>());
3    }

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

1public FormLoginConfigurer() {
2        super(new UsernamePasswordAuthenticationFilter(), null);
3        usernameParameter("username");
4        passwordParameter("password");
5    }

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

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

接着启动项目,查看控制台,发现多个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,自动跳转到了我们自定义的登陆页面:

security

security

扫描二维码关注公众号,回复: 5020722 查看本文章


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

 1@Override
 2protected void configure(HttpSecurity http) throws Exception {
 3    http.formLogin()
 4            .loginPage("/index")
 5            .loginProcessingUrl("/login")
 6            .and()
 7            .authorizeRequests()
 8            .antMatchers("/index", "/login", "/error", "/favicon.ico").permitAll()
 9            .anyRequest()
10            .authenticated()
11            .and()
12            .csrf()
13            .disable();
14}

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

security

security


并且后面带了一个error的参数,所以如果我们的login页面再做下处理,就能回显用户名密码错误了。然后我们继续输入我们一开始已经配置用户名密码,继续,这回出现了404,
security

security


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

 1@Override
 2protected void configure(HttpSecurity http) throws Exception {
 3    http.formLogin()
 4            .loginPage("/index")
 5            .loginProcessingUrl("/login")
 6            .successForwardUrl("/hello")
 7            .and()
 8            .authorizeRequests()
 9            .antMatchers("/index", "/login", "/error", "/favicon.ico").permitAll()
10            .anyRequest()
11            .authenticated()
12            .and()
13            .csrf()
14            .disable();
15}

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

security

security

自定义数据查询

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

内存中的数据源

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

security

security


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

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

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

security

security


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

db形式的数据源

  • 因为要使用数据库,那我们就选用spring-data jpa去操作数据库,首先引入依赖:
1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-data-jpa</artifactId>
4</dependency>
5<dependency>
6    <groupId>mysql</groupId>
7    <artifactId>mysql-connector-java</artifactId>
8</dependency>

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

 1spring:
 2  datasource:
 3    driver-class-name: com.mysql.jdbc.Driver
 4    username: root
 5    password: jason
 6    url:  jdbc:mysql://localhost:3306/springsecurity_demos?useSSL=false
 7
 8  jpa:
 9    show-sql: true
10    hibernate:
11      ddl-auto: update
12    properties:
13      hibernate:
14        format_sql: true

初始化sql脚本如下:

 1CREATE DATABASE springsecurity_demos;
 2USE springsecurity_demos;
 3
 4
 5SET NAMES utf8mb4;
 6SET FOREIGN_KEY_CHECKS = 0;
 7
 8-- ----------------------------
 9-- Table structure for role
10-- ----------------------------
11DROP TABLE IF EXISTS `role`;
12CREATE TABLE `role`  (
13                       `id` int(11) NOT NULL AUTO_INCREMENT,
14                       `role_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
15                       `description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
16                       PRIMARY KEY (`id`) USING BTREE
17) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
18
19-- ----------------------------
20-- Records of role
21-- ----------------------------
22INSERT INTO `role` VALUES (1, 'user', 'this is a user role.');
23INSERT INTO `role` VALUES (2, 'admin', 'this is a admin role.');
24
25-- ----------------------------
26-- Table structure for user
27-- ----------------------------
28DROP TABLE IF EXISTS `user`;
29CREATE TABLE `user`  (
30                       `id` int(11) NOT NULL AUTO_INCREMENT,
31                       `username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
32                       `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
33                       `age` int(11) NULL DEFAULT NULL,
34                       `address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
35                       `role_id` int(11) NOT NULL,
36                       PRIMARY KEY (`id`) USING BTREE
37) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
38
39-- ----------------------------
40-- Records of user
41-- ----------------------------
42INSERT INTO `user` VALUES (1, 'jsbintask', '$2a$10$WfUOGFJzAkPeRU3ZK2q6v.FNFcgIBrhixFNQ/htTKx71RK/OBMYaC', 22, 'China, Wuhan', 0);
43
44SET FOREIGN_KEY_CHECKS = 1;

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

 1@Table
 2@Entity
 3@Data
 4public class User {
 5    @Id
 6    @GeneratedValue(strategy = GenerationType.IDENTITY)
 7    private Integer id;
 8
 9    private String username;
10    private String password;
11
12    private Integer age;
13
14    private String address;
15
16    private Integer roleId;
17}
18
19@Repository
20public interface UserRepository extends JpaRepository<User, Integer> {
21    User findByUsername(String username);
22}
23
24@Entity
25@Table
26@Data
27public class Role {
28    @Id
29    @GeneratedValue(strategy = GenerationType.IDENTITY)
30    private Integer id;
31    private String roleName;
32    private String description;
33}
34
35@Repository
36public interface RoleRepository extends JpaRepository<Role, Integer> {
37}

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

 1@Data
 2@AllArgsConstructor
 3@NoArgsConstructor
 4public class AuthUser implements UserDetails {
 5    private String username;
 6    private String password;
 7    private List<Role> roles;
 8
 9    @Override
10    public Collection<? extends GrantedAuthority> getAuthorities() {
11        return roles.isEmpty() ? Collections.EMPTY_LIST :
12                (roles.parallelStream().map(role -> new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()));
13    }
14
15    @Override
16    public String getPassword() {
17        return password;
18    }
19
20    @Override
21    public String getUsername() {
22        return username;
23    }
24
25    @Override
26    public boolean isAccountNonExpired() {
27        return true;
28    }
29
30    @Override
31    public boolean isAccountNonLocked() {
32        return true;
33    }
34
35    @Override
36    public boolean isCredentialsNonExpired() {
37        return true;
38    }
39
40    @Override
41    public boolean isEnabled() {
42        return true;
43    }
44}

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

 1@Service
 2@Primary
 3public class CustomUserDetailsServiceImpl implements UserDetailsService {
 4    @Resource
 5    private UserRepository userRepository;
 6
 7    @Resource
 8    private RoleRepository roleRepository;
 9
10    @Override
11    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
12        User user = userRepository.findByUsername(username);
13
14        if (user == null) {
15            throw new UsernameNotFoundException("user: " + username + " is not found.");
16        }
17
18        return new AuthUser(user.getUsername(), user.getPassword(), roleRepository.findAllById(Collections.singletonList(user.getRoleId())));
19    }
20}

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

1@Bean
2public BCryptPasswordEncoder bCryptPasswordEncoder() {
3    return new BCryptPasswordEncoder();
4}

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

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

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

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

结束语:

本次我们手把手操作并且总结了springsecurity的一般用法,并且提出了很多值得注意的点。 那么接下来我们就站在一个高一点角度来分析一下springsecurity的架构吧。
如果你觉得对你有用,关注我!(微信公众号:jsbintask)。

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

猜你喜欢

转载自blog.csdn.net/Iperishing/article/details/86505120