上一章我们从源码角度探究了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同样效果):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login page</title>
</head>
<body>
This is login page from jsbintask
<form action="/login" method="post">
username: <input name="username" /><br/>
password: <input name="password" /><br/>
<button type="submit">submit</button>
</form>
</body>
</html>
这里请记住这个表单提交的地址/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<HttpSecurity> 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,自动跳转到了我们自定义的登陆页面:
然后点击提交,发现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();
}
这回正常了,我们输入错误的用户名,密码,果然,回到了原来的登陆页面,如下:
并且后面带了一个error的参数,所以如果我们的login页面再做下处理,就能回显用户名密码错误了。然后我们继续输入我们一开始已经配置用户名密码,继续,这回出现了404,
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页面:
security
自定义数据查询
经过上面的配置,我们一开始的目的达到了,自定义登陆页面,并且登陆成功后跳转到主界面,但是现在还有个问题是,我们的用户名密码是配置配置文件中的,这样肯定不行,
因为我们一般都是使用数据库的。接下来就是我们自定义数据源了。
内存中的数据源
上一篇博客我们已经通过源码分析了springboot在启动的时候帮我们初始化了一个在内存中的UserDetailService,如下:
那我们现在先来覆盖掉这个,回到先前的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不予通过。现在我们重新登陆,并且使用一开始配置文件中的用户名密码,发现此时已经不行了。 再用我们的新用户名密码,通过!
security
到这里,我们的自定义内存中的数据源就定义好了,接下来我们换成数据库中的形式。
db形式的数据源
- 因为要使用数据库,那我们就选用spring-data jpa去操作数据库,首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
接下来就是配置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, Integer> {
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<Role, Integer> {
}
好了,接下就是要与sprinsecurity结合了,在此之前,还有一点需要我们明白,springsecurity中对于用户的表示有自己的实体类相对应,它就是UserDetails,所以我们编写一个AuthUser实现这个接口:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthUser implements UserDetails {
private String username;
private String password;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> 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/,未经允许,禁止转载。
网友评论