在上一篇内容里介绍了,如何快速搭建一个有用户登录,授权才能访问的系统,如果我们使用基于springboot的全家桶,会让事情变得很简便。
- 这一篇我将为大家介绍如何去重构他以让它变的更加适合我们,首先我们将用户表的映射建设起来
设计表结构,我们现以较短的字段来做这个demo,盐在新版本里已经不用了,如果你仍想要结尾会给出方案。
CREATE TABLE IF NOT EXISTS `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(80) NOT NULL COMMENT '账号',
`password` varchar(80) NOT NULL COMMENT '密码',
`status` int(8) NOT NULL COMMENT '失效:0;正常:1',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`version` bigint(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
写一套用户相关程序,即service、dao,并新建如下方法并实现它:
/**
* 根据账号获取用户
*
* @param username - 账号名
* @return {@link User}
*/
User loadUserByUsername(String username);
插入以下数据,我是选择写一个testcase:
@Test
public void test01() {
User user = new User();
user.setUsername("JumperYu").setPassword("{noop}123456").setStatus(1).setCreateTime(new Date()).setUpdateTime(new Date()).setVersion(1L);
int rows = userMapper.save(user);
Assert.assertEquals(1, rows);
}
新建SecurityUserServiceImpl并实现org.springframework.security.core.userdetails.UserDetailsService
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.loadUserByUsername(username);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException(username.concat(" is not found"));
}
return new SecurityUserDetails(user);
}
新建SecurityUserDetails并实现org.springframework.security.core.userdetails.UserDetails
private String username;
private String password;
private boolean enabled;
private List<SimpleGrantedAuthority> authorities;
public SecurityUserDetails(User user) {
Assert.notNull(user, "user is required");
this.authorities = Lists.newArrayList();
this.username = user.getUsername();
this.password = user.getPassword();
this.enabled = user.getStatus() == 1;
}
我们再回到SecurityConfig,修改下上一章的配置
@Bean
@Override
public UserDetailsService userDetailsService() {
// 本地UserDetails
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("admin").password("{noop}123456").roles("admin").build());
// return manager;
return new SecurityUserServiceImpl();
}
@Bean
PasswordEncoder passwordEncoder() {
String idForEncode = "noop";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForEncode, NoOpPasswordEncoder.getInstance());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
我们仍然不使用盐,但是我们得配置上,最后我们修改下web层的UserController
@GetMapping("/")
@PreAuthorize("hasAnyAuthority()")
public ResponseEntity<String> index() {
SecurityUserDetails securityUserDetails = (SecurityUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ResponseEntity.ok("hello ".concat(securityUserDetails.getUsername()));
}
启动后我们访问下, http://localhost:8080/,正常情况下返回的是:
我们输入上面写入的账号密码,JumperYu / 123456,成功后会看到:
- 扩展几个问题
2-1. 不使用明文存储密码
我们再插入一条数据:
@Test
public void test02() {
User user = new User();
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String encodedPassword = encoder.encode("123456");
user.setUsername("xue").setPassword("{bcrypt}".concat(encodedPassword)).setStatus(1).setCreateTime(new Date()).setUpdateTime(new Date()).setVersion(1L);
int rows = userMapper.save(user);
Assert.assertEquals(1, rows);
}
往SecurityConfig插入一个encoder:
@Bean
@SuppressWarnings("deprecation")
PasswordEncoder passwordEncoder() {
String idForEncode = "noop";
Map<String, PasswordEncoder> encoders = new HashMap<>(4);
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put(idForEncode, NoOpPasswordEncoder.getInstance());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
可以删除掉jsessionid,然后再次访问验证即可
2-2. 我们实现的方法只有loadUserByUsername,框架是怎么辨识密码
我们知道springmvc的底层仍然是filter的实现,首先以debug模式启动application,并在org.springframework.security.web.FilterChainProxy#filter里面断点
重新删除cookie,重新访问http://localhost:8080/,我们会看到如下:
可见只要加入security的依赖,这个filter就会被加进来,它跑到一个自定义的filter(非servlet)
从上两个图可以看出,它会加载出默认的15个自定义filter,并且依次匹配,最终返回servlet filter的chain
默认情况下,所有路径都会被拦截,并且判定是否已经登录,否则返回默认登录页,这一章我们先不展开,
在登录页输入账号密码,会跳到org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
我们可以看到,它会在这里找一个非常关键的类authenticationManger,最终它会进到org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate,验证成功后把我们自定义返回的SecurityUser注入的SecurityContextHolder里面。
Okay,这一章先到此结束了,源码我放到https://github.com/JumperYu/springsecurity-demo
,建议大家以自我实践为主,下一章我将分享如何自定义登录页。