zoukankan      html  css  js  c++  java
  • Spring Security(一)自定义表单及认证授权(整合mybatis generator和mybatis)

    录:

    1、默认表单认证
    2、自定义表单登陆页
    3、自定义数据库模型的认证和授权
        3.1、使用 mysql 创建数据库
        3.2、mybatis generator 生成代码
        3.3、springboot 整合 mybatis
        3.4、实现 UserDetails
        3.5、实现 UserDetailsService

    1、默认表单认证   <--返回目录

      创建 springboot 项目,依赖:

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

      写一个测试 controller

    @RestController
    @RequestMapping("/index")
    public class IndexController {
        @RequestMapping("/test1")
        public String test1(String name, Integer age) {
            return "test1";
        }
    }

      启动项目,访问 http://localhost:8089/BootDemo/index/test1,弹出默认表单认证

       默认用户名为 user, 密码是动态生成并打印到控制台的一窜随机码。当然,用户名和密码可以在application.properties 中配置

    spring.security.user.name=test
    spring.security.user.password=123

    2、自定义表单登陆页   <--返回目录

      虽然spring security 自带的表单登陆页可以方便快速地启动,但大多数应用程序更希望提供自己的的表单登陆页,此时就需要自定义表单登陆页。

       WebSecurityConfig

    package com.oy;
    
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin().loginPage("/mylogin.html")
                .loginProcessingUrl("/login") // 指定处理登陆请求的路径
                .permitAll() // 登陆页和 "/login" 不设置权限
                .and().csrf().disable();
        }
    }

      表单登陆页

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    <h2>自定义表单登陆页</h2>
    <form action="login" method="post">
    用户名:<input type="text" name="username" /><br/>&nbsp;&nbsp;&nbsp;码:<input type="text" name="password" /><br/>
    <input type="submit" value="提交" />
    </form>
    </body>
    </html>
    View Code

      启动项目,访问 localhost:8089/BootDemo/index/test1,自动跳转到登陆页(浏览器地址为 http://localhost:8089/BootDemo/mylogin.html)。

      输入test/123, 登陆成功,拿到响应结果:

       如果输入错误的用户名或密码,响应结果(状态码 302,重定向到登陆页)

       对现在前后端分离的项目而言,重定向不在需要后端做,后端一般返回 json 数据,告知前端登陆成功与否,由前端决定如何处理后续逻辑,而非由服务器主动执行页面跳转。这在 Spring Security 中同样可以实现。

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().authenticated().and()
                .formLogin().loginPage("/mylogin.html")
                .loginProcessingUrl("/login") // 指定处理登陆请求的路径
                // 指定登陆成功时的处理逻辑
                .successHandler(new AuthenticationSuccessHandler() {
    
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                            Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        response.getWriter().write("{"code":0, "data":{}}");
                    }
                    
                })
                // 指定登陆失败时的处理逻辑
                .failureHandler(new AuthenticationFailureHandler() {
    
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                            AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        response.setStatus(401);
                        response.getWriter().write("{"code":0, "msg":"用户名或密码错误"}");
                    }
                    
                })
                
                .permitAll().and()
                .csrf().disable();
        }
    }

      其中,successHandler()方法带有一个 Authentication 参数,携带当前登陆用户名及其角色等信息;而 failureHandler() 方法携带一个AuthenticationException 异常参数。

    3、自定义数据库模型的认证和授权   <--返回目录

      前面沿用了 Spring Security 默认的安全机制:仅有一个用户,仅有一种角色。在实际开发中,这自然是无法满足要求的。

      编写三个 controller 进行测试,其中 /admin/api 下的内容是系统后台管理相关的API,必须拥有管理员权限(具有 "admin" 角色)才能访问; /user/api 必须在用户登陆并且具有 “user” 角色才能访问。

    @RestController
    @RequestMapping("/admin/api")
    public class AdminController {
        @GetMapping("/hello")
        public String hello() {
            return "hello, admin";
        }
    }
    @RestController
    @RequestMapping("/user/api")
    public class UserController {
        @GetMapping("/hello")
        public String hello() {
            return "hello, user";
        }
    }
    @RestController
    @RequestMapping("/app/api")
    public class AppController {
        @GetMapping("/hello")
        public String hello() {
            return "hello, app";
        }
    }
    View Code

      启动项目,访问 http://localhost:8089/BootDemo/user/api/hello,跳转到登陆页面,使用 test/123 登陆后。再次访问 http://localhost:8089/BootDemo/user/api/hello,此时服务器返回 403,表示用户授权失败(401 代表用户认证失败)。

    3.1、使用 mysql 创建数据库   <--返回目录

    create database security_test charset=utf8;
    use security_test;
    create table user (
        `id` bigint not null auto_increment,
        `username` varchar(100) not null,
        `password` varchar(100) not null,
        `enable` tinyint not null default 1 comment '用户是否可用,1:可用,2:禁用',
        `roles` varchar(500) comment '角色,多个角色用逗号隔开',
        primary key (`id`),
        key username (`username`)
    );
    
    insert into user(username,password,roles) values('admin','123','ROLE_user,ROLE_admin');
    insert into user(username,password,roles) values('user','123','ROLE_user');

    3.2、mybatis generator 生成代码   <--返回目录

      新建一个普通 Java Project

      Generator 类

    package com.oy;
    
    import java.io.File;
    import java.util.ArrayList;
    import java.util.List;
    
    import org.mybatis.generator.api.MyBatisGenerator;
    import org.mybatis.generator.config.Configuration;
    import org.mybatis.generator.config.xml.ConfigurationParser;
    import org.mybatis.generator.internal.DefaultShellCallback;
    
    public class Generator {
        public static void main(String[] args) throws Exception {
            List<String> warnings = new ArrayList<String>();
            boolean overwrite = true;
            File configFile = new File("src/com/oy/generator.xml");
            ConfigurationParser cp = new ConfigurationParser(warnings);
            Configuration config = cp.parseConfiguration(configFile);
            DefaultShellCallback callback = new DefaultShellCallback(overwrite);
            MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
            myBatisGenerator.generate(null);
        }
    }
    View Code

      generator.xml

    <?xml version="1.0" encoding="UTF-8"?>  
    <!DOCTYPE generatorConfiguration  
      PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"  
      "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    <generatorConfiguration>  
        <!-- 数据库驱动-->  
        <!-- <classPathEntry  location="C:Usersxxx.m2
    epositorymysqlmysql-connector-java5.1.38mysql-connector-java-5.1.38.jar"/> -->  
    
        <context id="DB2Tables"  targetRuntime="MyBatis3">
            <plugin type="com.qiukeke.mybatis.plugins.MySQLLimitPlugin"></plugin>
        
            <commentGenerator>  
                <property name="suppressDate" value="true"/>  
                <property name="suppressAllComments" value="true"/>  
            </commentGenerator>
            
            <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/security_test" 
                userId="root" password="123456">  
            </jdbcConnection> 
    
            <javaTypeResolver>  
                <property name="forceBigDecimals" value="false"/>  
            </javaTypeResolver>  
    
            
            <javaModelGenerator targetPackage="com.oy.model" targetProject=".src">  
                <property name="enableSubPackages" value="true"/>  
                <property name="trimStrings" value="true"/>  
            </javaModelGenerator>  
            <sqlMapGenerator targetPackage="com.oy.mapping" targetProject=".src">  
                <property name="enableSubPackages" value="true"/>  
            </sqlMapGenerator>  
            <javaClientGenerator type="XMLMAPPER" targetPackage="com.oy.dao" targetProject=".src">  
                <property name="enableSubPackages" value="true"/>  
            </javaClientGenerator>  
    
            <table tableName="user" domainObjectName="User" enableCountByExample="true" enableUpdateByExample="true" enableDeleteByExample="true" enableSelectByExample="true" selectByExampleQueryId="true"></table>
        </context>  
    
    </generatorConfiguration>
    View Code

      运行 Generator#main(),即可生成代码。

      上面的 jar 可以从 maven 仓库下载(建个 maven 工程,jar包下载到本地仓库,手动复制到上面的项目中)

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.2.6</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.36</version>
    </dependency>
    <!-- limit selection -->
    <dependency>
        <groupId>com.qiukeke</groupId>
        <artifactId>mybatis-generator-limit-plugin</artifactId>
        <version>1.0.4</version>
    </dependency>
    View Code

      mybatis-generator-limit-plugin-1.0.4.jar 是个 mybatis 分页插件,会在 实体 example 类中添加 limit、offset 两个字段(同时 mapping.xml 文件中也加入了分页功能)

    3.3、springboot 整合 mybatis   <--返回目录

      依赖:

    <!-- mybatis 相关 [start] -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!-- <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.2.6</version>
    </dependency> -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.36</version>
    </dependency>
    <!-- mybatis 相关 [end] -->
    View Code

      配置:

    #datasource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security_test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.tomcat.min-idle=5
    
    ##################### MyBatis相关配置 [start] #####################
    #MyBatis映射文件
    mybatis.mapper-locations=classpath:com/oy/mapping/*.xml
    #扫描生成实体的别名,需要和注解@Alias联合使用
    mybatis.type-aliases-package=com.oy.model
    #MyBatis配置文件,当你的配置比较复杂的时候,可 以使用
    #mybatis.config-location=
    #级联延迟加载。true:开启延迟加载
    mybatis.configuration.lazy-loading-enabled=true
    #积极的懒加载。false:按需加载
    mybatis.configuration.aggressive-lazy-loading=false
    ##################### MyBatis相关配置 [end]  ######################
    View Code

      在主 springboot 配置类上添加注解 @MapperScan 扫描 dao 接口生成代理对象

    @SpringBootApplication
    @MapperScan("com.oy.dao")
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    View Code

      写测试代码,进行测试:

    @RestController
    @RequestMapping("/app/api")
    public class AppController {
        
        @Autowired
        private UserService userService;
        
        @RequestMapping("/{id}")
        public String findById(@PathVariable Long id) {
            User dbUser = userService.getUserById(id);
            return JSONObject.toJSONString(dbUser);
        }
        
        @GetMapping("/hello")
        public String hello() {
            return "hello, app";
        }
    }
    
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Override
        public User getUserById(Long id) {
            return userDao.selectByPrimaryKey(id);
        }
    
    }
    View Code

      访问 http://localhost:8089/BootDemo/app/api/1,结果:

     3.4、实现 UserDetails   <--返回目录

      Spring Security 中,使用 UserDetails 来封装用户信息,包含一系列在验证时要用到的信息,比如用户名、密码、权限及其他信息,Spring Security 会根据这些信息来校验。

      UserDetails 有这样一些方法:

    public interface UserDetails extends Serializable {
        /**
         * Returns the authorities granted to the user. Cannot return <code>null</code>.
         *
         * @return the authorities, sorted by natural key (never <code>null</code>)
         */
        Collection<? extends GrantedAuthority> getAuthorities();
    
        /**
         * Returns the password used to authenticate the user.
         *
         * @return the password
         */
        String getPassword();
    
        /**
         * Returns the username used to authenticate the user. Cannot return <code>null</code>.
         *
         * @return the username (never <code>null</code>)
         */
        String getUsername();
    
        /**
         * Indicates whether the user's account has expired. An expired account cannot be
         * authenticated.
         *
         * @return <code>true</code> if the user's account is valid (ie non-expired),
         * <code>false</code> if no longer valid (ie expired)
         */
        boolean isAccountNonExpired();
    
        /**
         * Indicates whether the user is locked or unlocked. A locked user cannot be
         * authenticated.
         *
         * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
         */
        boolean isAccountNonLocked();
    
        /**
         * Indicates whether the user's credentials (password) has expired. Expired
         * credentials prevent authentication.
         *
         * @return <code>true</code> if the user's credentials are valid (ie non-expired),
         * <code>false</code> if no longer valid (ie expired)
         */
        boolean isCredentialsNonExpired();
    
        /**
         * Indicates whether the user is enabled or disabled. A disabled user cannot be
         * authenticated.
         *
         * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
         */
        boolean isEnabled();
    }

      为了程序的可维护性,我没有修改 mybatis generator 根据数据库 user 表映射生成的 User 类,而是写一个新类继承 User 类,并实现 UserDetails 接口。

    package com.oy.security;
    
    import java.util.Collection;
    import java.util.List;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import com.oy.model.User;
    
    public class SecurityUser extends User implements UserDetails {
    
        private static final long serialVersionUID = 1L;
        private List<GrantedAuthority> authorities;
    
        public void setAuthorities(List<GrantedAuthority> authorities) {
            this.authorities = authorities;
        }
    
        /**
         * getAuthorities() 方法本身对应的是 roles 字段,但由于结构不一样,
         * 所以此类中添加一个 authorities 字段,后面自己手动设置
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    
        @Override
        public String toString() {
            return "SecurityUser [id=" + getId() + ", username=" + getUsername() + ", password=" 
                    + getPassword() + ", enable=" + getEnable() + ", roles=" + getRoles() + "]";
        }
        
        
    }
    View Code

    3.5、实现 UserDetailsService   <--返回目录

      UserDetailsService 仅定义了一个 loadUserByUsername() 方法,用于获取一个 UserDetails 对象。UserDetails 对象包含一系列在验证时会用到的信息,包括用户名、密码、权限等。

    package com.oy.security;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import com.oy.dao.UserDao;
    import com.oy.model.User;
    import com.oy.model.UserExample;
    
    /**
     * @author oy
     * @version 1.0
     * @date 2020年4月14日
     * @time 上午10:25:02
     */
    @Service
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private UserDao userDao;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 从数据库尝试获取该用户
            UserExample example = new UserExample();
            UserExample.Criteria criteria = example.createCriteria();
            criteria.andUsernameEqualTo(username);
            List<User> userList = userDao.selectByExample(example);
            
            if (userList == null || userList.size() == 0) {
                throw new RuntimeException("该用户不存在");
            }
            
            SecurityUser sUser = getUser(userList.get(0));
            System.out.println("sUser: " + sUser);
            // 将数据库 roles 字段解析成 UserDetails 的权限集
            sUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sUser.getRoles()));
            return sUser;
        }
        
        /**
         * 读取 User 对象的属性,封装一个 SecurityUser 对象
         * @param user
         * @return
         */
        private SecurityUser getUser(User user) {
            if (user == null) {
                throw new RuntimeException("该用户不存在");
            }
            
            SecurityUser sUser = new SecurityUser();
            sUser.setEnable(user.getEnable());
            sUser.setId(user.getId());
            sUser.setPassword(user.getPassword());
            sUser.setRoles(user.getRoles());
            sUser.setUsername(user.getUsername());
            return sUser;
        }
    
    }
    View Code

    3.6、其他

      1)测试时报 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 这个错误。原因是5.x 版本后默认开启了委派密码编码器,所以本文暂时将密码编码器设置为 noOpPasswordEncoder。

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    View Code

      PasswordEncoder 接口有两个方法

    public interface PasswordEncoder {
        String encode(CharSequence var1);
        boolean matches(CharSequence var1, String var2);
    }
    View Code

      实际开发中,可以使用

    @Bean
    PasswordEncoder passowrdEncoder() {
        return new BCryptPasswordEncoder();
    }
    View Code

      所以,当用户注册,保存用户的密码时,从 Spring 容器中获取 PasswordEncoder 实例,调用 PasswordEncoder 实例的 encode() 方法对密码进行加密(数据库存的是加密后的密码)。

      2)UserDetails 接口中包含的一些方法,比如 isEnabled() 可以用来校验用户状态(是否删除),isAccountNonLocked() 可以用来校验用户状态(是否冻结)等。可以根据业务场景进行实现,比如:

    @Override
    public boolean isEnabled() {
        if (getEnable().intValue() == 2) {
            return false;
        }
        return true;
    }
    View Code

     ===================================================================================================

    至此,代码写完了。当使用 admin/123 登陆后,再次访问 http://localhost:8089/BootDemo/admin/api/1, 就不会返回 403 了。

    总结一下认证和授权过程:

      1) 用户使用 admin/123 登陆时,Spring Security 调用 UserDetailsService#loadUserByUsername() 读取数据库,查出是否有 admin 这个用户名,有则读取,并将用户名、密码、权限封装成一个 UserDetails 对象返回。然后,Spring Security 根据UserDetails 对象的密码与表单传来的密码比较。

      2) 当访问非公开权限的资源时,调用UserDetails#getAuthorities() 进行权限校验。

    本文内容包括:

    处理用户信息获取逻辑 UserDetailsService
    处理用户校验逻辑 UserDetails
    处理密码加密解密 PasswordEncoder
    
    自定义登陆页面
    自定义登陆成功处理 AuthenticationSuccessHandler
    自定义登陆失败处理 AuthenticationFailureHandler

    参考:

      1)《Spring Security 实战》-- 陈木鑫

      2)spring boot security 实战

  • 相关阅读:
    善用js 异步引入,大幅提升vue站点加载速度,媲美大厂流畅性
    react hooks useState 赋值优化解决方案
    vue hash模式下的微信授权详解
    图片之间没加空格有间隙的解决方案
    React 父组件重新渲染,子组件不需要渲染的三种性能优化方式(PureComponent,memo,shouldComponentUpdate);
    uniapp历史模式history配置
    vue下请求数据地址的封装
    h5 下ios适配底部小黑条,简单解决方案,只需一步
    overflow:hidden 失效问题
    h5下数字,字母不换行的解决方案
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/12695264.html
Copyright © 2011-2022 走看看