基于数据库的身份认证
一、创建项目
创建一个 SpringBoot 模块项目,选择相关依赖:
先搭建项目正常访问,在pom.xml中,先把Spring Security依赖注释
1. <dependency> 2. <groupId>org.springframework.boot</groupId> 3. <artifactId>spring-boot-starter-security</artifactId> 4. </dependency>
在MySQL数据库中创建一张用户表,id主键自增,并添加两个用户如下:
代码:
创建entity实体
在MySQL数据库中创建一张用户表,id主键自增,并添加两个用户如下:
代码:
创建entity实体
1. @Data 2. public class UserInfo { 3. private int id; 4. private String username; 5. private String password; 6. private String role; 7. }
创建mapper接口
1. @Mapper 2. @Repository 3. public interface UserInfoMapper { 4. @Select("select * from user where username = #{username}") 5. UserInfo getUserInfoByUsername(String username); 6. } 7. 创建UserInfoService.java文件 8. 9. @Service 10. public class UserInfoService { 11. @Autowired 12. private UserInfoMapper userInfoMapper; 13. 14. public UserInfo getUserInfo(String username){ 15. return userInfoMapper.getUserInfoByUsername(username); 16. } 17. }
创建HelloController.java文件
1. @RestController 2. public class HelloController { 3. @Autowired 4. private UserInfoService userInfoService; 5. 6. @GetMapping("/get-user") 7. public UserInfo getUser(@RequestParam String username){ 8. return userInfoService.getUserInfoByUsername(username); 9. } 10. }
配置文件application.yml中设置MySQL连接配置和MyBatis配置
1. spring: 2. datasource: 3. url: jdbc:mysql://localhost:3306/security?useSSL=FALSE&serverTimezone=UTC 4. username: root 5. password: root 6. driver-class-name: com.mysql.cj.jdbc.Driver 7. logging: 8. level: 9. com.example.bdatabaserole.mapper: debug # 打印sql语句
以上配置好后,启动项目,访问localhost:8080/get-user?username=user,正常获取到用户信息:
下面开始使用Spring Security安全验证:
二、Spring Security基于数据库认证
把pom.xml中的Spring Security依赖注释去掉:
1. <dependency> 2. <groupId>org.springframework.boot</groupId> 3. <artifactId>spring-boot-starter-security</artifactId> 4. </dependency>
要从数据库读取用户信息进行身份认证,需要新建类实现UserDetailService接口重写loadUserByUsername方法:
1. @Component 2. public class CustomUserDetailsService implements UserDetailsService { 3. @Autowired 4. private UserInfoService userInfoService; 5. 6. /** 7. * 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定 8. */ 9. @Autowired 10. private PasswordEncoder passwordEncoder; 11. 12. @Override 13. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 14. // 通过用户名从数据库获取用户信息 15. UserInfo userInfo = userInfoService.getUserInfo(username); 16. if (userInfo == null) { 17. throw new UsernameNotFoundException("用户不存在"); 18. } 19. 20. // 得到用户角色 21. String role = userInfo.getRole(); 22. 23. // 角色集合 24. List<GrantedAuthority> authorities = new ArrayList<>(); 25. // 角色必须以`ROLE_`开头,数据库中没有,则在这里加 26. authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); 27. 28. return new User( 29. userInfo.getUsername(), 30. // 因为数据库是明文,所以这里需加密密码 31. passwordEncoder.encode(userInfo.getPassword()), 32. authorities 33. ); 34. } 35. }
创建Security的配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,并重写configure(auth)方法:
1. @EnableWebSecurity 2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 3. @Autowired 4. private MyUserDatailService userDatailService; 5. 6. /** 7. * 指定加密方式 8. */ 9. @Bean 10. public PasswordEncoder passwordEncoder(){ 11. // 使用BCrypt加密密码 12. return new BCryptPasswordEncoder(); 13. } 14. 15. @Override 16. protected void configure(AuthenticationManagerBuilder auth) throws Exception { 17. auth 18. // 从数据库读取的用户进行身份认证 19. .userDetailsService(userDatailService) 20. .passwordEncoder(passwordEncoder()); 21. } 22. }
上面设置完后,重新启动,在登录页面就可以输入数据库中的用户名/密码了。
三、角色访问控制
上面设置后,可以使用数据库中的用户名/密码登录,还获取到了用户的角色。通过用户的角色,可以限制用户的请求访问:
开启方法的访问权限,需要在WebSecurityConfig添加
@EnableGlobalMethodSecurity注解
1. @EnableWebSecurity 2. @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证 3. public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 4. ... 5. }
修改UserController.java类,增加方法的访问权限
1. @RestController 2. public class HelloController { 3. @Autowired 4. private UserInfoService userInfoService; 5. 6. @GetMapping("/get-user") 7. public UserInfo getUser(@RequestParam String username){ 8. return userInfoService.getUserInfo(username); 9. } 10. 11. @PreAuthorize("hasAnyRole('user')") // 只能user角色才能访问该方法 12. @GetMapping("/user") 13. public String user(){ 14. return "user角色访问"; 15. } 16. 17. @PreAuthorize("hasAnyRole('admin')") // 只能admin角色才能访问该方法 18. @GetMapping("/admin") 19. public String admin(){ 20. return "admin角色访问"; 21. } 22. }
配置完后,重新启动程序,在登录页面中:
输入user/user123登录,该用户角色为user,可以访问localhost:8080/user,不能访问localhost:8080/admin;
再重启程序输入admin/admin123登录,角色为admin,能访问localhost:8080/admin,不能访问localhost:8080/user。
四、密码加密保存
前面的用户密码都是手动添加的,所以数据库中是明文显示,在实际开发中,都是需要加密保存的。
下面模拟简单注册用户,加密保存密码:
UserInfoMapper.java类中添加插入用户
1. @Mapper 2. @Repository 3. public interface UserMapper { 4. ... 5. // 插入用户 6. @Insert("insert into user(username, password, role) value(#{username}, #{password}, #{role})") 7. int insertUserInfo(UserInfo userInfo); 8. }
UserInfoService.java类中添加插入方法,注意要加密密码
1. @Service 2. public class UserInfoService { 3. ... 4. 5. @Autowired 6. private PasswordEncoder passwordEncoder; 7. 8. ... 9. 10. public int insertUser(UserInfo userInfo){ 11. // 加密密码 12. userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword())); 13. return userInfoMapper.insertUserInfo(userInfo); 14. } 15. 16. }
修改HelloController.java,增加添加用户接口
1. @RestController 2. public class HelloController { 3. ... 4. 5. @PostMapping("/add-user") 6. public int addUser(@RequestBody UserInfo userInfo){ 7. return userInfoService.insertUser(userInfo); 8. } 9. }
配置完后,启动服务,使用Postman发送POST请求来添加用户:
点击Send按钮后,添加失败,不会返回成功1,看到红框的状态码显示401 Unauthorized,说明无权限,需要登录,但注册用户是不用登录的,所以就需要注册用户的请求无需身份验证:
修改WebSecurityConfig配置类,重写configure(HttpSecurity http)方法,配置允许注册用户的请求访问:
1. @EnableWebSecurity 2. @EnableGlobalMethodSecurity(prePostEnabled = true) 3. public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 4. ... 5. @Override 6. protected void configure(HttpSecurity http) throws Exception { 7. http 8. .authorizeRequests() 9. .antMatchers(HttpMethod.POST, "/add-user").permitAll() // 允许post请求/add-user,而无需认证 10. .anyRequest().authenticated() // 所有请求都需要验证 11. .and() 12. .formLogin() // 使用默认的登录页面 13. .and() 14. .csrf().disable();// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数 15. } 16. }
配置允许POST请求/add-user访问后,再在Postman发送请求就可以成功了:
图片
查看数据库数据,添加的用户密码已加密:
使用加密密码登录,需要修改CustomUserDetailsService类,之前从数据库拿到明文密码后需要加密,现在数据库里面的密码已经加密了,就不用加密了:
1. @Component 2. public class MyUserDatailService implements UserDetailsService { 3. //@Autowired 4. //private PasswordEncoder passwordEncoder; 5. ... 6. @Override 7. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 8. ... 9. return new User( 10. user.getUsername(), 11. // 数据库密码已加密,不用再加密 12. user.getPassword(), 13. authorities 14. ); 15. } 16. }
浏览器访问localhost:8080/user,输入zeng/zeng123登录即可。
五、密码修改
UserInfoMapper.java类中添加更新用户密码操作:
1. @Mapper 2. @Repository 3. public interface UserMapper { 4. ... 5. @Update("update user set password = #{newPwd} where username = #{username}") 6. int updatePwd(String username, String newPwd); 7. } 8. UserInfoService.java类中添加更新密码的操作方法: 9. 10. @Service 11. public class UserInfoService { 12. ... 13. public int updatePwd(String oldPwd, String newPwd) { 14. // 获取当前登录用户信息(注意:没有密码的) 15. UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 16. String username = principal.getUsername(); 17. 18. // 通过用户名获取到用户信息(获取密码) 19. UserInfo userInfo = userInfoMapper.getUserInfoByUsername(username); 20. 21. // 判断输入的旧密码是正确 22. if (passwordEncoder.matches(oldPwd, userInfo.getPassword())) { 23. // 不要忘记加密新密码 24. return userInfoMapper.updatePwd(username, passwordEncoder.encode(newPwd)); 25. } 26. return 0; 27. } 28. }
HelloController.java类增加修改用户密码接口:
1. @RestController 2. public class HelloController { 3. @PutMapping("/updatePwd") 4. public int updatePwd(@RequestBody Map<String, String> map){ 5. return userInfoService.updatePwd(map.get("oldPwd"), map.get("newPwd")); 6. } 7. }
启动程序,因为需要登录,且修改密码请求方式为PUT请求,所以无法使用Postman发起请求,可以使用谷歌浏览器的插件Restlet Client:
先浏览器输入http://localhost:8080/login登录用户zeng/zeng123:
在Restlet Client中进行PUT更新请求:
这里更新后,需要重启后就可以使用新密码登录了。
5.用户角色多对多关系
上面的设置后,能基本实现了身份认证和角色授权了,但还是有一点不足:
我们前面用户表中,用户和角色是绑定一起,用户就只有一个角色了,但实际上,用户可能拥有多个角色,角色拥有多个用户,是多对多的关系,所以需要重新设置用户表和角色表。
创建普通项目运行
IDEA创建一个 Spring Initializr模块项目,名为b-database-manytomany-role,选择Spring Security和web依赖,其他按需选择Lombok、MySQL、JDBC和MyBatis。
先按照普通项目创建起来正常访问,在pom.xml中,先把Spring Security依赖注释
1. <!--<dependency>--> 2. <!--<groupId>org.springframework.boot</groupId>--> 3. <!--<artifactId>spring-boot-starter-security</artifactId>--> 4. <!--</dependency>-->
新建表,主键id全部都是自增。
用户user2表
1. CREATE TABLE user2 ( uid int(11) NOT NULL AUTO_INCREMENT, 2. username varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci 3. NULL DEFAULT NULL, password varchar(255) CHARACTER SET utf8 4. COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (uid) USING 5. BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 6. COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
角色role2表,指定了2个角色
1. CREATE TABLE role2 ( rid int(11) NOT NULL AUTO_INCREMENT, 2. role varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL 3. DEFAULT NULL, PRIMARY KEY (rid) USING BTREE ) ENGINE = InnoDB 4. AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci 5. ROW_FORMAT = Dynamic;
用户角色关系user2_role2表
1. CREATE TABLE user2_role2 ( id int(11) NOT NULL AUTO_INCREMENT, 2. uid int(11) NULL DEFAULT NULL, rid int(11) NULL DEFAULT NULL, 3. PRIMARY KEY (id) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 10 4. CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
初始化数据:
– 添加1个用户 INSERT INTO user2 VALUES (1, ‘user’, ‘user123’);
– 添加2个角色 INSERT INTO role2 VALUES (1, ‘user’); INSERT INTO role2 VALUES (2, ‘admin’);
– 1个用户,拥有2个角色 INSERT INTO user2_role2 VALUES (1, 1, 1); INSERT INTO user2_role2 VALUES (2, 1, 2);
使用Navicat可视化表的数据结构为:
创建entity实体类
User类
1. @Data 2. public class User { 3. private Integer uid; 4. private String username; 5. private String password; 6. }
Role类
1. @Data 2. public class Role { 3. private Integer rid; 4. private String role; 5. }
创建DTO类
因为用户和角色是多对多关系,需要在用户中含有角色的对象,角色中含有用户的对象,创建DTO类而不再entity类中添加,是因为entity类属性是和表字段一一对应的,一般不推荐在entity类中添加与表字段无关的属性。
新建dto包,在包下创建如下类:
UserDTO类
// 注意,多对多不要用@Data,因为ToString会相互调用,导致死循环
1. @Setter 2. @Getter 3. public class UserDTO extends User { 4. private Set<Role> roles; 5. }
RoleDTO类(目前用不到,可不建)
// 注意,多对多不要用@Data,因为ToString会相互调用,导致死循环
1. @Setter 2. @Getter 3. public class RoleDTO extends Role { 4. private Set<User> users; 5. }
添加UserMapper.java类
1. @Mapper 2. @Repository 3. public interface UserMapper { 4. // 查询用户 5. UserDTO selectUserByUsername(@Param("username") String username); 6. }
UserMapper.xml。因为需要关联查询,所有使用xml方式
1. <?xml version="1.0" encoding="UTF-8" ?> 2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > 3. <mapper namespace="com.example.bdatabasemanytomanyrole.mapper.UserMapper"> 4. 5. <resultMap id="userRoleMap" type="com.example.bdatabasemanytomanyrole.dto.UserDTO"> 6. <id property="uid" column="uid"/> 7. <result property="username" column="username"/> 8. <result property="password" column="password"/> 9. <collection property="roles" ofType="com.example.bdatabasemanytomanyrole.dto.RoleDTO"> 10. <id property="rid" column="rid"/> 11. <result property="role" column="role"></result> 12. </collection> 13. </resultMap> 14. <select id="selectUserByUsername" resultMap="userRoleMap"> 15. select user2.uid, user2.username, user2.password, role2.rid, role2.role 16. from user2, user2_role2, role2 17. where user2.username=#{username} 18. and user2.uid = user2_role2.uid 19. and user2_role2.rid = role2.rid 20. </select> 21. </mapper>
UserService.java
1. @Service 2. public class UserService { 3. @Autowired 4. private UserMapper userMapper; 5. 6. public UserDTO getUser(String username){ 7. return userMapper.selectUserByUsername(username); 8. } 9. }
UserController.java
1. @RestController 2. public class UserController { 3. @Autowired 4. private UserService userService; 5. 6. @GetMapping("/get-user") 7. public UserDTO getUser(@RequestParam String username){ 8. return userService.getUser(username); 9. } 10. }
上面完成后,启动项目,访问localhost:8080/get-user?username=user,查询到的用户信息为:
引入Spring Security安全验证
把pom.xml中的Spring Security依赖注释去掉:
1. <dependency> 2. <groupId>org.springframework.boot</groupId> 3. <artifactId>spring-boot-starter-security</artifactId> 4. </dependency>
此时,再重新启动程序,访问localhost:8080/get-user?username=user时,会跳转到登录页面。此时默认的登录用户名为user,密码在启动时打印在控制台。
从数据库中获取用户、密码进行登录:
添加MyUserDetailsService.java,实现UserDetailsService,重写loadUserByUsername方法:
1. @Component 2. public class MyUserDetailsService implements UserDetailsService { 3. @Autowired 4. private UserService userService; 5. 6. @Override 7. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 8. UserDTO user = userService.getUser(username); 9. if (user == null) { 10. throw new UsernameNotFoundException("用户不存在"); 11. } 12. // 添加用户拥有的多个角色 13. List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); 14. Set<Role> roles = user.getRoles(); 15. for (Role role : roles) { 16. grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRole())); 17. } 18. 19. return new User( 20. user.getUsername(), 21. // 数据库中密码没加密,需加密 22. new BCryptPasswordEncoder().encode(user.getPassword()), 23. grantedAuthorities 24. ); 25. } 26. }
添加WebSecurityConfig.java,继承WebSecurityConfigurerAdapter,重写configure(AuthenticationManagerBuilder auth)方法:
1. @EnableWebSecurity 2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 3. @Autowired 4. private MyUserDetailsService userDetailsService; 5. @Bean 6. public PasswordEncoder passwordEncoder(){ 7. return new BCryptPasswordEncoder(); 8. } 9. @Override 10. protected void configure(AuthenticationManagerBuilder auth) throws Exception { 11. auth 12. .userDetailsService(userDatailService) 13. .passwordEncoder(passwordEncoder()); 14. } 15. }
重新启动,可以使用user/user123登录了。
查看登录用户信息
要查看登录用户信息,我们可以在UserController中添加方法:
1. @RestController 2. public class UserController { 3. @Autowired 4. private UserService userService; 5. 6. @GetMapping("/get-user") 7. public UserDTO getUser(@RequestParam String username){ 8. return userService.getUser(username); 9. } 10. 11. /** 12. * 查看登录用户信息 13. */ 14. @GetMapping("/get-auth") 15. public Authentication getAuth(){ 16. return SecurityContextHolder.getContext().getAuthentication(); 17. } 18. }
重新启动登录后,访问localhost:8080/get-auth,返回