赞
踩
前面学习了Security简单的认证和授权,而我们使用的登录用户是在内存中或者配置文件中定义的,而实际项目中我们都是在数据库中定义用户,接下来我们开始学习如何将用户数据保存到数据库。
SpringSecurity支持多种不同的数据源,这些不同的数据源最终都将被封装成UserDetailsService的实例,可以自己封装,也可以使用系统默认提供的UserDetailsService实例,例如前面介绍的InMemoryUserDetailsManager;在UserDetailsService的实现类中,除了InMemoryUserDetailsManager之外,还有一个JdbcUserDetailsManager,使用JdbcUserDetailsManager可以让我们通过 JDBC 的方式将数据库和SpringSecurity连接起来,但是JdbcUserDetailsManager使用起来不是很方便,感兴趣的小伙伴可以自己去了解一下。
这里我们自己封装UserDetailsService实例,为了操作方便我们使用Mybatis-Plus来完成数据库操作,数据库使用H2Database,大家可以使用MySql都是一样的。
可以参考我这篇文章SpringBoot整合MyBatis-Plus+Druid,来整合SpringBoot、MyBatis-Plus、H2Database
创建新的SpringBoot项目,引入以下相关依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.70</version>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.3.2</version>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-generator</artifactId>
- <version>3.3.2</version>
- </dependency>
- <dependency>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity-engine-core</artifactId>
- <version>2.2</version>
- </dependency>
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- <version>5.3.5</version>
- </dependency>
配置application.properties配置文件
- # DataSource Config
- spring.datasource.driver-class-name=org.h2.Driver
- # 配置本地数据库地址,按需修改
- # DATABASE_TO_LOWER=TRUE;-要求数据库名称小写 CASE_INSENSITIVE_IDENTIFIERS=TRUE;-要求对字段的大小写不敏感
- spring.datasource.url=jdbc:h2:D:/symon/project/db/security3;CASE_INSENSITIVE_IDENTIFIERS=TRUE;
- spring.datasource.username=symon
- spring.datasource.password=123456
- # 指定Schema (DDL)脚本
- spring.datasource.schema=classpath:db/schema.sql
- # 指定Data (DML)脚本
- spring.datasource.data=classpath:db/data.sql
- # 指定schema要使用的Platform
- spring.datasource.platform=h2
- # 是否启用h2控制台
- spring.h2.console.enabled=true
- # 配置h2控制台访问地址,http://localhost:8080/h2
- spring.h2.console.path=/h2
-
- # MyBatis-Plus配置
- mybatis-plus.mapper-locations=classpath:mapper/*.xml
在resources下创建db文件夹,并创建schema.sql和data.sql脚本,分别用来填写建表语句和初始化几条数据
- #schema.sql
- CREATE TABLE IF NOT EXISTS sys_user
- (
- ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
- USERNAME varchar(50) NOT NULL COMMENT '用户名',
- PASSWORD varchar(128) NOT NULL COMMENT '密码',
- STATUS INT(2) NOT NULL COMMENT '状态 0锁定 1有效',
- CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
- MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
- PRIMARY KEY (ID)
- );
-
- CREATE TABLE IF NOT EXISTS sys_role (
- ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
- ROLE_CODE varchar(100) NOT NULL COMMENT '角色标识',
- ROLE_NAME varchar(100) NOT NULL COMMENT '角色名称',
- CREATE_TIME datetime(0) NOT NULL COMMENT '创建时间',
- MODIFY_TIME datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
- PRIMARY KEY (ID)
- );
-
- CREATE TABLE IF NOT EXISTS sys_user_role (
- ID bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色关联ID',
- USER_ID bigint(20) NOT NULL COMMENT '用户ID',
- ROLE_ID bigint(20) NOT NULL COMMENT '角色ID',
- PRIMARY KEY (ID)
- ) ;
-
- #data.sql
- DELETE FROM sys_user;
- INSERT INTO sys_user VALUES (1, 'symon', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
- INSERT INTO sys_user VALUES (2, 'test', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 1, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
- INSERT INTO sys_user VALUES (3, 'test1', '$2a$10$Dm9tmMfvISVWTmrCM9WwUeSwgdwYSgU48zkqbhEcuDgl40SJuDYsu', 0, '2020-07-21 20:39:22', '2020-07-21 20:44:42');
-
- DELETE FROM sys_role;
- INSERT INTO sys_role VALUES (1, 'root', '管理员', '2020-07-21 20:39:22', '2020-07-21 20:44:42');
- INSERT INTO sys_role VALUES (2, 'user', '普通用户', '2020-07-21 20:39:22', '2020-07-21 20:44:42');
-
- DELETE FROM sys_user_role;
- INSERT INTO sys_user_role VALUES (1, 1, 1);
- INSERT INTO sys_user_role VALUES (2, 2, 2);
- INSERT INTO sys_user_role VALUES (3, 3, 2);
启动项目,之后访问登录h2控制台http://localhost:8080/h2,即可看到表已经创建成功
然后终止项目,使用MyBatis-Plus的代码生成器快速生成代码,可参考我这篇文章SpringBoot整合MyBatis-Plus+Druid
生成之后如下,我们可以看到实体类、mapper、service已经自动生成好了
创建SysUserDetails类实现UserDetails接口中的方法
- public class SysUserDetails implements UserDetails {
- private SysUserDTO user;
- public SysUserDetails(SysUserDTO user) {
- this.user = user;
- }
- @Override
- public Collection<? extends GrantedAuthority> getAuthorities() {
- List<SimpleGrantedAuthority> authorities = new ArrayList<>();
- if (!CollectionUtils.isEmpty(user.getRoleList())) {
- for (SysRole sysRole : user.getRoleList()) {
- authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRole.getRoleCode()));
- }
- }
- return authorities;
- }
- @Override
- public String getPassword() {return user.getPassword();}
- @Override
- public String getUsername() {return user.getUsername();}
- @Override
- public boolean isAccountNonExpired() {return true;}
- @Override
- public boolean isAccountNonLocked() {return true;}
- @Override
- public boolean isCredentialsNonExpired() {return true;}
- @Override
- public boolean isEnabled() {return Objects.equals(1, user.getStatus());}
- }
其中:
SysUserDTO的内容如下:
- @Data
- public class SysUserDTO {
- private Long id;
- private String username;
- private String password;
- private Integer status;
- private LocalDateTime createTime;
- private LocalDateTime modifyTime;
- //用户关联角色
- private List<SysRole> roleList;
- }
UserDetails中有四个字段,accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个字段分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用。
getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。
创建SysUserDetailService实现UserDetailsService接口中的loadUserByUsername方法
- @Service
- public class SysUserDetailService implements UserDetailsService {
- @Autowired
- private SysUserService sysUserService;
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- SysUserDTO user = sysUserService.getUserDetail(username);
- if (user == null){
- throw new UsernameNotFoundException("用户名不存在!");
- }
- return new SysUserDetails(user);
- }
- }
其中:
SysUserService是刚刚自动生成的类,其中有一个手动创建的方法,其内容如下
- @Service
- public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
- @Autowired
- private SysUserMapper sysUserMapper;
- @Autowired
- private SysRoleMapper sysRoleMapper;
- @Override
- public SysUserDTO getUserDetail(String username) {
- Wrapper<SysUser> wrapper = new QueryWrapper<SysUser>().eq("username", username);
- SysUser sysUser = sysUserMapper.selectOne(wrapper);
- if (sysUser != null) {
- SysUserDTO user = BeanUtil.copyProperties(sysUser, SysUserDTO.class);
- List<SysRole> roleList = sysRoleMapper.selectByUserId(sysUser.getId());
- user.setRoleList(roleList);
- return user;
- } else {
- return null;
- }
- }
- }
getUserDetail方法中的SysRoleMapper及其映射xml文件的的方法的内容如下,该方法查询用户关联的角色
- List<SysRole> selectByUserId(@Param("userId") Long userId);
-
- <select id="selectByUserId" resultType="com.example.security3.entity.SysRole">
- select r.<include refid="Base_Column_List" />
- from SYS_ROLE r
- JOIN SYS_USER_ROLE ur on ur.ROLE_ID = r.ID
- WHERE ur.USER_ID = #{userId}
- </select>
UserDetailsService中的loadUserByUsername方法的参数就是用户在登录的时候传入的用户名,根据用户名去查询用户信息(查出来之后,系统会自动进行密码比对)
然后引入security依赖,并配置SecurityConfig,可以采用之前的配置,不过需要稍稍修改一下
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
SecurityConfig配置
- @Configuration
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- public static final String CONTENT_TYPE = "application/json;charset=utf-8";
- @Autowired
- private SysUserDetailService sysUserDetailService;
-
- @Bean
- PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder(10);//BCryptPasswordEncoder加密
- }
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(sysUserDetailService).passwordEncoder(passwordEncoder());
- }
- @Override
- public void configure(WebSecurity web) throws Exception {
- web.ignoring().antMatchers("/h2/**");
- }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.cors().and()
- .authorizeRequests()
- .antMatchers("/user/**").hasRole("user")
- .antMatchers("/root/**").hasRole("root")
- .anyRequest().authenticated()
- .and()
- .formLogin()
- .successHandler((req, resp, auth) -> {
- resp.setContentType(CONTENT_TYPE);
- PrintWriter out = resp.getWriter();
- out.write(JSON.toJSONString(ResponseDTO.success("登录成功!")));
- out.flush();
- out.close();
- })
- .failureHandler((req, resp, exception) -> {
- resp.setContentType(CONTENT_TYPE);
- PrintWriter out = resp.getWriter();
- String exeMsg = "登录失败!";
- if (exception instanceof LockedException) {
- exeMsg = "账户已被锁定!";
- } else if (exception instanceof CredentialsExpiredException) {
- exeMsg = "密码已过期!";
- } else if (exception instanceof AccountExpiredException) {
- exeMsg = "账户已过期!";
- } else if (exception instanceof DisabledException) {
- exeMsg = "账户已被禁用!";
- } else if (exception instanceof BadCredentialsException) {
- exeMsg = "用户名或者密码输入错误,请重新输入!";
- }
- out.write(JSON.toJSONString(ResponseDTO.error(exeMsg)));
- out.flush();
- out.close();
- })
- .and()
- .logout()
- .logoutSuccessHandler((req, resp, auth) -> {
- resp.setContentType(CONTENT_TYPE);
- PrintWriter out = resp.getWriter();
- out.write(JSON.toJSONString(ResponseDTO.success("注销成功!再见!")));
- out.flush();
- out.close();
- })
- .permitAll()
- .and()
- .exceptionHandling()
- .authenticationEntryPoint((req, resp, exception) -> {
- resp.setContentType(CONTENT_TYPE);
- PrintWriter out = resp.getWriter();
- out.write(JSON.toJSONString(ResponseDTO.unauthenticated("未登录,请重新登录!")));
- out.flush();
- out.close();
- })
-
- .accessDeniedHandler((req, resp, exception) -> {
- resp.setContentType(CONTENT_TYPE);
- PrintWriter out = resp.getWriter();
- out.write(JSON.toJSONString(ResponseDTO.unauthorized("无权限!")));
- out.flush();
- out.close();
- })
- .and().csrf().disable();
- }
-
- @Bean
- RoleHierarchy roleHierarchy() {
- RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
- hierarchy.setHierarchy("ROLE_root > ROLE_user");
- return hierarchy;
- }
- }
其中:
configure(AuthenticationManagerBuilder auth)方法我们使用了自己定义的UserDetailService,并使用了BCryptPasswordEncoder将密码加密
启动之前,要创建测试类
- @RestController
- public class TestController {
- @GetMapping("/test")
- public String test() {
- return "Hello World!";
- }
- @GetMapping("/user/test")
- public String user(){
- return "user权限";
- }
- @GetMapping("/root/test")
- public String root(){
- return "root权限";
- }
- }
启动类中要记得添加MapperScan
- @SpringBootApplication
- @MapperScan("com.example.security3.dao")
- public class Security3Application {
- public static void main(String[] args) {
- SpringApplication.run(Security3Application.class, args);
- }
- }
然后启动项目,对接口进行测试
注意,这里我初始化的密码是123456加密之后的,可自行加密
- public static void main(String[] args) {
- BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
- System.out.println(bCryptPasswordEncoder.encode("123456"));
- }
另外test1用户在初始化时STATUS设置了0,在登陆验证时UserDetails的isEnabled返回了false,所以认为该账户被禁用,抛出DisabledException异常,被failureHandler捕获之后返回了自定义的结果。
- @Override
- public boolean isEnabled() {return Objects.equals(1, user.getStatus());}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。