Spring Security数据库身份认证和角色授权

基于数据库的身份认证

一、创建项目

创建一个 SpringBoot 模块项目,选择相关依赖:

图片

先搭建项目正常访问,在pom.xml中,先把Spring Security依赖注释

<!--<dependency>-->
    <!--<groupId>org.springframework.boot</groupId>-->
    <!--<artifactId>spring-boot-starter-security</artifactId>-->
<!--</dependency>-->

在MySQL数据库中创建一张用户表,id主键自增,并添加两个用户如下:

[图片上传中...(1569337219103.png-663d9b-1572663758313-0)]


图片

代码:

  1. 创建entity实体

    @Data
    public class UserInfo {
        private int id;
        private String username;
        private String password;
        private String role;
    }
    
  2. 创建mapper接口

    @Mapper
    @Repository
    public interface UserInfoMapper {
        @Select("select * from user where username = #{username}")
        UserInfo getUserInfoByUsername(String username);
    }
    
  3. 创建UserInfoService.java文件

    @Service
    public class UserInfoService {
        @Autowired
        private UserInfoMapper userInfoMapper;
    
        public UserInfo getUserInfo(String username){
            return userInfoMapper.getUserInfoByUsername(username);
        }
    }
    
  4. 创建HelloController.java文件

    @RestController
    public class HelloController {
        @Autowired
        private UserInfoService userInfoService;
        
        @GetMapping("/get-user")
        public UserInfo getUser(@RequestParam String username){
            return userInfoService.getUserInfoByUsername(username);
        }
    }
    
  5. 配置文件application.yml中设置MySQL连接配置和MyBatis配置

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/security?useSSL=FALSE&serverTimezone=UTC
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
    logging:
      level:
        com.example.bdatabaserole.mapper: debug  # 打印sql语句
    

以上配置好后,启动项目,访问localhost:8080/get-user?username=user,正常获取到用户信息:

图片

下面开始使用Spring Security安全验证:

二、Spring Security基于数据库认证

  1. pom.xml中的Spring Security依赖注释去掉:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 要从数据库读取用户信息进行身份认证,需要新建类实现UserDetailService接口重写loadUserByUsername方法:

    @Component
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private UserInfoService userInfoService;
        
        /**
         * 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
         */
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 通过用户名从数据库获取用户信息
            UserInfo userInfo = userInfoService.getUserInfo(username);
            if (userInfo == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
    
            // 得到用户角色
            String role = userInfo.getRole();
    
            // 角色集合
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 角色必须以`ROLE_`开头,数据库中没有,则在这里加
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    
            return new User(
                    userInfo.getUsername(),
                 // 因为数据库是明文,所以这里需加密密码
                    passwordEncoder.encode(userInfo.getPassword()),
                    authorities
            );
        }
    }
    
  3. 创建Security的配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,并重写configure(auth)方法:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDatailService userDatailService;
    
        /**
         * 指定加密方式
         */
        @Bean
        public PasswordEncoder passwordEncoder(){
            // 使用BCrypt加密密码
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth
                     // 从数据库读取的用户进行身份认证
                    .userDetailsService(userDatailService)
                    .passwordEncoder(passwordEncoder());
        }
    }
    

上面设置完后,重新启动,在登录页面就可以输入数据库中的用户名/密码了。

三、角色访问控制

上面设置后,可以使用数据库中的用户名/密码登录,还获取到了用户的角色。通过用户的角色,可以限制用户的请求访问:

  1. 开启方法的访问权限,需要在WebSecurityConfig添加@EnableGlobalMethodSecurity注解

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...
    }
    
  2. 修改UserController.java类,增加方法的访问权限

    @RestController
    public class HelloController {
        @Autowired
        private UserInfoService userInfoService;
    
        @GetMapping("/get-user")
        public UserInfo getUser(@RequestParam String username){
            return userInfoService.getUserInfo(username);
        }
    
        @PreAuthorize("hasAnyRole('user')") // 只能user角色才能访问该方法
        @GetMapping("/user")
        public String user(){
            return "user角色访问";
        }
    
        @PreAuthorize("hasAnyRole('admin')") // 只能admin角色才能访问该方法
        @GetMapping("/admin")
        public String admin(){
            return "admin角色访问";
        }
    }
    

配置完后,重新启动程序,在登录页面中:

  • 输入user/user123登录,该用户角色为user,可以访问localhost:8080/user,不能访问localhost:8080/admin
  • 再重启程序输入admin/admin123登录,角色为admin,能访问localhost:8080/admin,不能访问localhost:8080/user

四、密码加密保存

前面的用户密码都是手动添加的,所以数据库中是明文显示,在实际开发中,都是需要加密保存的。

下面模拟简单注册用户,加密保存密码:

  1. UserInfoMapper.java类中添加插入用户

    @Mapper
    @Repository
    public interface UserMapper {
     ...
        // 插入用户
        @Insert("insert into user(username, password, role) value(#{username}, #{password}, #{role})")
        int insertUserInfo(UserInfo userInfo);
    }
    
  2. UserInfoService.java类中添加插入方法,注意要加密密码

    @Service
    public class UserInfoService {
        ...
        
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        ...
    
        public int insertUser(UserInfo userInfo){
            // 加密密码
            userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
            return userInfoMapper.insertUserInfo(userInfo);
        }
    }
    
  3. 修改HelloController.java,增加添加用户接口

    @RestController
    public class HelloController {
        ...
            
        @PostMapping("/add-user")
        public int addUser(@RequestBody UserInfo userInfo){
            return userInfoService.insertUser(userInfo);
        }
    }
    

配置完后,启动服务,使用Postman发送POST请求来添加用户:

图片

点击Send按钮后,添加失败,不会返回成功1,看到红框的状态码显示401 Unauthorized,说明无权限,需要登录,但注册用户是不用登录的,所以就需要注册用户的请求无需身份验证:

  1. 修改WebSecurityConfig配置类,重写configure(HttpSecurity http)方法,配置允许注册用户的请求访问:

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     ...    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers(HttpMethod.POST, "/add-user").permitAll() // 允许post请求/add-user,而无需认证
                    .anyRequest().authenticated() // 所有请求都需要验证
                    .and()
                    .formLogin() // 使用默认的登录页面
                    .and()
                    .csrf().disable();// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
        }
    }
    

配置允许POST请求/add-user访问后,再在Postman发送请求就可以成功了:

图片

查看数据库数据,添加的用户密码已加密:

图片

使用加密密码登录,需要修改CustomUserDetailsService类,之前从数据库拿到明文密码后需要加密,现在数据库里面的密码已经加密了,就不用加密了:

@Component
public class MyUserDatailService implements UserDetailsService {
    //@Autowired
    //private PasswordEncoder passwordEncoder;
    ...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ...
        return new User(
                user.getUsername(),
                // 数据库密码已加密,不用再加密
                user.getPassword(),
                authorities
        );
    }
}

浏览器访问localhost:8080/user,输入zeng/zeng123登录即可。

五、密码修改

  1. UserInfoMapper.java类中添加更新用户密码操作:

    @Mapper
    @Repository
    public interface UserMapper {
     ...
     @Update("update user set password = #{newPwd} where username = #{username}")
        int updatePwd(String username, String newPwd);
    }
    
  2. UserInfoService.java类中添加更新密码的操作方法:

    @Service
    public class UserInfoService { 
        ...
        public int updatePwd(String oldPwd, String newPwd) {
            // 获取当前登录用户信息(注意:没有密码的)
            UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String username = principal.getUsername();
    
            // 通过用户名获取到用户信息(获取密码)
            UserInfo userInfo = userInfoMapper.getUserInfoByUsername(username);
    
            // 判断输入的旧密码是正确
            if (passwordEncoder.matches(oldPwd, userInfo.getPassword())) {
                // 不要忘记加密新密码
                return userInfoMapper.updatePwd(username, passwordEncoder.encode(newPwd));
            }
            return 0;
        }
    }
    
  3. HelloController.java类增加修改用户密码接口:

    @RestController
    public class HelloController {    
        @PutMapping("/updatePwd")
        public int updatePwd(@RequestBody Map<String, String> map){
            return userInfoService.updatePwd(map.get("oldPwd"), map.get("newPwd"));
        }
    }
    

启动程序,因为需要登录,且修改密码请求方式为PUT请求,所以无法使用Postman发起请求,可以使用谷歌浏览器的插件Restlet Client

  1. 先浏览器输入http://localhost:8080/login登录用户zeng/zeng123:

    图片
  1. Restlet Client中进行PUT更新请求:

    图片

这里更新后,需要重启后就可以使用新密码登录了。

5.用户角色多对多关系

上面的设置后,能基本实现了身份认证和角色授权了,但还是有一点不足:

  • 我们前面用户表中,用户和角色是绑定一起,用户就只有一个角色了,但实际上,用户可能拥有多个角色,角色拥有多个用户,是多对多的关系,所以需要重新设置用户表和角色表。
创建普通项目运行

IDEA创建一个 Spring Initializr模块项目,名为b-database-manytomany-role,选择Spring Securityweb依赖,其他按需选择LombokMySQLJDBCMyBatis

先按照普通项目创建起来正常访问,在pom.xml中,先把Spring Security依赖注释

<!--<dependency>-->
    <!--<groupId>org.springframework.boot</groupId>-->
    <!--<artifactId>spring-boot-starter-security</artifactId>-->
<!--</dependency>-->
  1. 新建表,主键id全部都是自增。

    • 用户user2

      CREATE TABLE `user2`  (
        `uid` int(11) NOT NULL AUTO_INCREMENT,
        `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        PRIMARY KEY (`uid`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
      
    • 角色role2表,指定了2个角色

      CREATE TABLE `role2`  (
        `rid` int(11) NOT NULL AUTO_INCREMENT,
        `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        PRIMARY KEY (`rid`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
      
    • 用户角色关系user2_role2

      CREATE TABLE `user2_role2`  (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `uid` int(11) NULL DEFAULT NULL,
        `rid` int(11) NULL DEFAULT NULL,
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
      
  2. 初始化数据:

    -- 添加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可视化表的数据结构为:

[图片上传中...(9245320-2a06dc7e17a7496e.png-18b99e-1572663906608-0)]


图片
  1. 创建entity实体类

    • User

      @Data
      public class User {
          private Integer uid;
          private String username;
          private String password;
      }
      
    • Role

      @Data
      public class Role {
          private Integer rid;
          private String role;
      }
      
  2. 创建DTO

    因为用户和角色是多对多关系,需要在用户中含有角色的对象,角色中含有用户的对象,创建DTO类而不再entity类中添加,是因为entity类属性是和表字段一一对应的,一般不推荐在entity类中添加与表字段无关的属性。

    新建dto包,在包下创建如下类:

    • UserDTO

      // 注意,多对多不要用@Data,因为ToString会相互调用,导致死循环
      @Setter
      @Getter
      public class UserDTO extends User {
          private Set<Role> roles;
      }
      
    • RoleDTO类(目前用不到,可不建)

      // 注意,多对多不要用@Data,因为ToString会相互调用,导致死循环
      @Setter
      @Getter
      public class RoleDTO extends Role {
          private Set<User> users;
      }
      
  3. 添加UserMapper.java

    @Mapper
    @Repository
    public interface UserMapper {
        // 查询用户
        UserDTO selectUserByUsername(@Param("username") String username);
    }
    

    UserMapper.xml。因为需要关联查询,所有使用xml方式

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.example.bdatabasemanytomanyrole.mapper.UserMapper">
    
        <resultMap id="userRoleMap" type="com.example.bdatabasemanytomanyrole.dto.UserDTO">
            <id property="uid" column="uid"/>
            <result property="username" column="username"/>
            <result property="password" column="password"/>
            <collection property="roles" ofType="com.example.bdatabasemanytomanyrole.dto.RoleDTO">
                <id property="rid" column="rid"/>
                <result property="role" column="role"></result>
            </collection>
        </resultMap>
        <select id="selectUserByUsername" resultMap="userRoleMap">
            select user2.uid, user2.username, user2.password, role2.rid, role2.role
            from user2, user2_role2, role2
            where user2.username=#{username} 
            and user2.uid = user2_role2.uid 
            and user2_role2.rid = role2.rid
        </select>
    </mapper>
    
  4. UserService.java

    @Service
    public class UserService {
        @Autowired
        private UserMapper userMapper;
    
        public UserDTO getUser(String username){
            return userMapper.selectUserByUsername(username);
        }
    }
    
  5. UserController.java

    @RestController
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/get-user")
        public UserDTO getUser(@RequestParam String username){
            return userService.getUser(username);
        }
    }
    

上面完成后,启动项目,访问localhost:8080/get-user?username=user,查询到的用户信息为:

图片
引入Spring Security安全验证

pom.xml中的Spring Security依赖注释去掉:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

此时,再重新启动程序,访问localhost:8080/get-user?username=user时,会跳转到登录页面。此时默认的登录用户名为user,密码在启动时打印在控制台。

从数据库中获取用户、密码进行登录:

  1. 添加MyUserDetailsService.java,实现UserDetailsService,重写loadUserByUsername方法:

    @Component
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private UserService userService;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            UserDTO user = userService.getUser(username);
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
            // 添加用户拥有的多个角色
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            Set<Role> roles = user.getRoles();
            for (Role role : roles) {
                grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRole()));
            }
    
            return new User(
                    user.getUsername(),
                 // 数据库中密码没加密,需加密
                    new BCryptPasswordEncoder().encode(user.getPassword()),
                    grantedAuthorities
            );
        }
    }
    
  2. 添加WebSecurityConfig.java,继承WebSecurityConfigurerAdapter,重写configure(AuthenticationManagerBuilder auth)方法:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailsService userDetailsService;
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
     }        
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                    .userDetailsService(userDatailService)
                    .passwordEncoder(passwordEncoder());
        }
    }
    

重新启动,可以使用user/user123登录了。

查看登录用户信息

要查看登录用户信息,我们可以在UserController中添加方法:

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/get-user")
    public UserDTO getUser(@RequestParam String username){
        return userService.getUser(username);
    }

    /**
     * 查看登录用户信息
     */
    @GetMapping("/get-auth")
    public Authentication getAuth(){
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

重新启动登录后,访问localhost:8080/get-auth,返回:

图片
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容