一、基本环境搭建
父pom依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent>
1. 添加pom依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2. 创建测试用Controller
@RestController public class TestController { @GetMapping("getData") public String getData() { return "date"; } }
3. 创建SpringBoot启动类并run
@SpringBootApplication public class SpringBootTestApplication { public static void main(String[] args) { SpringApplication.run(SpringBootTestApplication.class, args); } }
4. 测试
访问http://127.0.0.1:8080/getData,由于我们开启了SpringSecurity且当前是未登录状态,页面会被302重定向到http://127.0.0.1:8080/login,页面如下:
用户名:user,密码可以在控制台输出中找到:
输入正确的用户名和密码后点击Login按钮即被重新302到http://127.0.0.1:8080/getData并显示查询数据:
这表示我们的接口已经被spring保护了。
那么肯定会有小伙伴吐槽了,这么复杂的密码,鬼才及得住,所以...
二、为Spring Security设定用户名和密码
为了解决复杂密码的问题,我们可以在application.yml中做如下设定:
spring:
security:
user:
name: user
password: 123
这样我们就可以通过用户名user密码123来访问http://127.0.0.1:8080/getData接口了。
然后肯定又有小伙伴吐槽了,整个系统就一个用户么?有哪个系统是只有一个用户的?所以...
三、为Spring Security设定多个用户
如果想要给Spring Security设定多个用户可用,则新建一个class:
@Configuration
@EnableWebSecurity public class WebSecurityConfig { @Bean public UserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User
.withUsername("admin")
.password("admin")
.passwordEncoder(encoder::encode)
.roles("")
.build()
); manager.createUser(User
.withUsername("guest")
.password("guest")
.passwordEncoder(encoder::encode)
.roles("")
.build()
); return manager; } }
- 注意需要注解@EnableWebSecurity
- InMemoryUserDetailsManager:顾名思义,将用户名密码存储在内存中的用户管理器。我们通过这个管理器增加了两个用户,分别是:用户名admin密码admin,用户名guest密码guest。
做完如上更改后重启应用,再次访问http://127.0.0.1:8080/getData,输入admin/admin或guest/guest即可通过身份验证并正常使用接口了。
看到这肯定又有小伙伴要吐槽了:用户数据直接硬编码到代码里是什么鬼!我要把用户放在数据库!所以...
四、SpringSecurity+Mysql
想要使用数据库,那么我们可以
1. 增加如下依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
2. 配置数据库连接
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.2.12:3306/test?characterEncoding=utf8 username: root password: onceas
3. 创建测试用表结构及数据
drop table if exists test.user; create table test.user ( id int auto_increment primary key, username varchar(50), password varchar(50) ); insert into test.user(id, username, password) values (1, 'admin', 'admin'); insert into test.user(id, username, password) values (2, 'guest', 'guest');
我们创建了用户信息表,并插入两个用户信息,用户名/密码依然是admin/admin、guest/guest
4. entity、dao、service
public class User { private int id; private String username; private String password; // get set ... }
@Repository public class LoginDao { private final JdbcTemplate jdbcTemplate; @Autowired public LoginDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public List<User> getUserByUsername(String username) { String sql = "select id, username, password from user where username = ?"; return jdbcTemplate.query(sql, new String[]{username}, new BeanPropertyRowMapper<>(User.class)); } }
@Service public class LoginService { private final LoginDao loginDao; @Autowired public LoginService(LoginDao loginDao) { this.loginDao = loginDao; } public List<User> getUserByUsername(String username) { return loginDao.getUserByUsername(username); } }
5. 调整WebSecurityConfig
@Bean public UserDetailsService userDetailsService() { return username -> { List<UserEntity> users = loginService.getUserByUsername(username); if (users == null || users.size() == 0) { throw new UsernameNotFoundException("用户名未找到"); } String password = users.get(0).getPassword(); PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); String passwordAfterEncoder = passwordEncoder.encode(password); return User.withUsername(username).password(passwordAfterEncoder).roles("").build(); }; }
做完如上更改后重启应用,再次访问http://127.0.0.1:8080/getData,输入admin/admin或guest/guest即可通过身份验证并正常使用接口了。
关于UserDetailsService,有些东西要说明下:
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);
上面这两句代码是在对用户密码进行加密。为什么要这样子呢?看到这肯定又有小伙伴会吐槽:数据库存储铭文密码是什么鬼!对,Spring也是尽量在帮助开发者避免这个事情。所以SpringSecurity在进行密码比对的时候需要开发者提供加密后的密码。我们上面的写法其实是不合理的,实际情况应该是数据库中存储密文密码,然后将数据库中的密码直接传给User.password()就可以了。
6. 关于SpringSecurity加密后的密文格式
我们可以通过打断点的方式或者增加
System.out.println(username + "---->>>" + passwordAfterEncoder);
来查看下,如果admin/admin被登录时候,passwordAfterEncoder的值是什么?输出结果:
admin---->>>{bcrypt}$2a$10$d4VkiIfP7MyNSipjLtQ0Keva4ST6U6Fnw77iiv39IGnGswptqWRG.
guest---->>>{bcrypt}$2a$10$8jRMbiGzFIS4GU3SWAm83eWgFO29EEb5QhXOEkPEaabw5Oiy/jxUC
可以看出加密后的密码可以分为两部分
- {}内描述了加密算法,这里为bcrypt算法。
- {}后面即为密文密码,这里是包含盐的。
所以SpringSecurity的工作原理就是:当用户输入用户名和密码点击Login以后,SpringSecurity先通过调用我们自定义的UserDetailsService获取到加密后密码,然后根据{}里的内容获知加密算法,再将用户输入的密码按照该算法进行加密,最后再与{}后的密文密码比对即可获知用户凭据是否有效。
通过查看PasswordEncoderFactories的源码,我们可以知道SpringEncoder工具可以提供哪些加密算法:
public static PasswordEncoder createDelegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new LdapShaPasswordEncoder()); encoders.put("MD4", new Md4PasswordEncoder()); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); }
其中LdapShaPasswordEncoder、Md4PasswordEncoder、MessageDigestPasswordEncoder、NoOpPasswordEncoder、StandardPasswordEncoder已经不建议使用了。SpringSecurity认为:
Digest based password encoding is not considered secure. //基于摘要的密码编码被认为是不安全的
五 、权限控制
以上内容我们只解决了用户登录问题,但是实际开发中仅仅完成用户登录是不够的,我们还需要用户授权及授权验证。由于我们已经将用户信息存储到数据库里了,那么姑且我们也将权限信息存储在数据库吧。
1. 准备数据库表及测试数据
drop table if exists test.role; create table test.role ( id int auto_increment primary key, role varchar(50) ); drop table if exists test.permission; create table test.permission ( id int auto_increment primary key, permission varchar(50) ); drop table if exists test.user_r_role; create table test.user_r_role ( userid int, roleid int ); drop table if exists test.role_r_permission; create table test.role_r_permission ( roleid int, permissionid int ); drop table if exists test.user_r_permission; create table test.user_r_permission ( userid int, permissionid int ); insert into test.role(id, role) values (1, 'adminRole'); insert into test.role(id, role) values (2, 'guestRole'); insert into test.permission(id, permission) values (1, 'permission1'); insert into test.permission(id, permission) values (2, 'permission2'); insert into test.permission(id, permission) values (3, 'permission3'); insert into test.permission(id, permission) values (4, 'permission4'); insert into test.user_r_role(userid, roleid) values (1, 1); insert into test.user_r_role(userid, roleid) values (2, 2); insert into test.role_r_permission(roleid, permissionid) values (1, 1); insert into test.role_r_permission(roleid, permissionid) values (1, 2); insert into test.user_r_permission(userid, permissionid) values (1, 3); insert into test.user_r_permission(userid, permissionid) values (1, 4); insert into test.user_r_permission(userid, permissionid) values (2, 3); insert into test.user_r_permission(userid, permissionid) values (2, 4);