0
点赞
收藏
分享

微信扫一扫

二 . spring-security 自定义用户登录处理

工程Demo结构


工程demo地址:https://gitee.com/decent-cat/security.git

一. 使用

1.1 添加依赖包

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
  </parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Spring-security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.17</version>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--social-web-->
        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-web</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>
</dependencies>

1.1 编写自己的登录页面

该登录页面包包含了验证码, 记住我功能

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/vue.js" type="text/javascript"></script>
    <script src="js/axios.min.js" type="text/javascript"></script>
    <script src="js/elementIndex.js" type="text/javascript"></script>
    <link href="css/elementIndex.css" rel="stylesheet" type="text/css" />
    <style>
        * {
            padding: 0;
            margin: 0;
        }
        .el-header {
            background: #d3dce6;
        }
    </style>
    <script>
        function refreshImageCode(imageTag) {
            imageTag.src = '/validate/code?date=' + new Date();
        }
    </script>

</head>
<body>
    <div id="app">
        <el-container>
            <el-header>XXXX</el-header>
            <el-main>
                <el-row>
                    <el-col :span="8" :offset="8">
                        <el-card class="box-card">
                            <el-tabs v-model="activeName">
                                <el-tab-pane label="用户名登录" name="usernameLogin">
                                    <el-form :model="countLoginForm" ref="countLoginForm" label-width="70px">
                                        <el-form-item label="用户名">
                                            <el-input v-model="countLoginForm.username"></el-input>
                                        </el-form-item>
                                        <el-form-item label="密 码">
                                            <el-input v-model="countLoginForm.password"></el-input>
                                        </el-form-item>
                                        <el-form-item label="验证码">
                                            <el-input v-model="countLoginForm.validateCode" style="width: 75%;"></el-input>
                                            <img style="width:23%; position: relative; top: 12px;" onclick="refreshImageCode(this)" src="/validate/code">
<!--                                            <img style="width:23%; position: relative; top: 12px;" @click="getValidateCodeImage" :src="validateCodeImage">-->
                                        </el-form-item>
                                        <el-form-item>
                                            <el-checkbox v-model="countLoginForm['remember-me']" label="记住我"></el-checkbox>
                                        </el-form-item>
                                        <el-form-item>
                                            <el-button @click.prevent="accounLogin" type="primary" style="width: 100%;">登录</el-button>
                                        </el-form-item>
                                    </el-form>
                                </el-tab-pane>
                                <el-tab-pane label="短信码登录" name="smsLogin">
                                    <el-form :model="smsLoginForm" :rules="rules" ref="smsLoginForm" label-width="70px">
                                        <el-form-item label="手机号" prop="mobile">
                                            <el-input v-model="smsLoginForm.mobile"></el-input>
                                        </el-form-item>
                                        <el-form-item label="验证码">
                                            <el-input v-model="smsLoginForm.smsCode" style="width: 68%;"></el-input>
                                            <el-button style="width: 30%;" :disabled="sendSmsBtnEnable" @click.prevent="sendSmsCode">{{sendSmsBtnText}}</el-button>
                                        </el-form-item>
                                        <el-form-item>
                                            <el-button @click.prevent="smsLogin" type="primary" style="width: 100%;">登录</el-button>
                                        </el-form-item>
                                    </el-form>
                                </el-tab-pane>
                            </el-tabs>
                        </el-card>
                    </el-col>
                </el-row>
            </el-main>
        </el-container>
    </div>
</body>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                rules: {
                    mobile: [
                        {required: true, message: '手机号不能为空', trigger: 'blur'},
                        {pattern: /^1([3578]\d|9[19])\d{8}$/, message: '目前只支持中国大陆手机号', trigger: 'blur'}
                    ]
                },
                countLoginForm: {
                    username: '',
                    password: '',
                    'remember-me': false,
                    validateCode: '',
                },
                smsLoginForm: {
                    mobile: '',
                    smsCode: '',
                },
                sendSmsBtnEnable: false,   //发送短信按钮的可用状态
                sendSmsBtnText: '发送短信',  //发送短信按钮上显示文字
                activeName: 'usernameLogin',  //默认账户登录表单激活
                showErrorMsg: false,   //错误信息是否展示
                errorMsg: '',  //错误信息内容
                validateCodeImage: '',
            }
        },
        mounted(){
            this.getValidateCodeImage(); //获取图片验证码
        },
        methods: {
            //账号登录
            accounLogin() {
                axios({
                    url: '/authentication/form',
                    method: 'post',
                    params: this.countLoginForm
                }).then(resp => {
                    let datas = resp.data
                    if(datas.code > 0) {
                        window.location.href = "/user";
                    }else {
                        /**
                        this.showErrorMsg = true;
                        this.errorMsg = resp.data.msg;
                         */
                        this.$notify({
                            title: '警告',
                            message: resp.data.msg,
                            type: 'warning'
                        });
                    }
                });
            },

            sendSmsCode() {
                this.$refs['smsLoginForm'].validate(valid => {
                    if(valid) {
                        this.secondDown(60);
                        // 判断手机号不能为空
                        axios.get('/sms/code?mobile=' + this.smsLoginForm.mobile);
                    }else {
                        return;
                    }
                })


            },

            // 获取图片验证码,方便在纯粹的前后端分离的时候使用
            getValidateCodeImage(){
                axios({
                    url: '/validate/code',
                    method: 'get',
                }).then(resp => {
                    this.validateCodeImage = "data:image/jpeg;base64," + resp.data;
                })
            },

            //短信登录
            smsLogin(loginType) {

                axios({
                    url: '/authentication/sms',
                    method: 'post',
                    params: this.smsLoginForm
                }).then(resp => {
                    let datas = resp.data
                    if(datas.code > 0) {
                        window.location.href = "/user";
                    }else {
                        /**
                         this.showErrorMsg = true;
                         this.errorMsg = resp.data.msg;
                         */
                        this.$notify({
                            title: '警告',
                            message: resp.data.msg,
                            type: 'warning'
                        });
                    }
                })
            },

            //发送短信倒计时
            secondDown(seconds) {
                if(seconds <= 0) {
                    this.sendSmsBtnEnable = false;  //60秒倒计时后按钮可用
                    this.sendSmsBtnText = '发送短信';
                }else {
                    this.sendSmsBtnEnable = true; // 按钮不可用

                    this.sendSmsBtnText = '已发送(' + seconds + ")";

                    seconds--;

                    setTimeout(() => this.secondDown(seconds), 1000);
                }
            }

        }
    })
</script>
</html>

1.2 自定义用户认证

// 该类的作用是处理用户登录名和密码
@Component
public class UserSecurityService implements UserDetailsService {
    //日志打印
    private static Logger logger = LoggerFactory.getLogger(UserSecurityService.class);
    //数据库用户数据获取类 @Service
    @Resource
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("用户名:" + username); //打印用户名
        // 用户信息类, 包含账号和密码信息
        SysUser sysUser = null;
        try{
            //根据传入用户名去数据库查找用户信息, 密码不能直接存在数据库, 要经过加密, 常用加密类为: BCryptPasswordEncoder
            sysUser = sysUserService.getSysUserByUsename(username);
        }catch (EmptyResultDataAccessException exception) {  //没有对应的用户名异常
            //登录各种失败采取抛错误交由MyAuthenticationFailureHandlertongyi8处理
            throw new UsernameNotFoundException("用户或密码错误");
        }
        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名或密码错误");
        }else {
            /**
             * org.springframework.security.core.userdetails.User
             * User第一参数是:用户名
             * 第二个参数是:pssword, 是从数据库查出来的
             * 第三个参数是: 权限
             */
            User user =  null;
            try{
                user = new User(username,
                        sysUser.getPassword(),
                        // 配置用户访问权限
                        Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
            }catch (InternalAuthenticationServiceException exception) {
                throw exception;  // 在此处,将异常接着往外抛,抛给AuthenticationFailureHandler处理
            }
            return user;
        }
    }
}

用户数据表

create table sys_user(
    id int primary key,
    username char(20),
    password char(100)
) ENGINE = MyIsam;

1.3 登录成功处理

要实现用户登录成功处理,需要实现AuthenticationSuccessHandler这个接口,然后实现接口中的方法:

//登录成功处理
@Component
public class MySuccessAuthenticationHandler implements AuthenticationSuccessHandler {
    //该bean是springmvc启动的时候实例化的一个对象,纳入到容器中
    @Autowired
    private ObjectMapper objectMapper;
    //登录成功执行的方法
    //authentication中包含了用户的各种信息,包括UserDetail信息
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        Map<String, Object> map = new HashMap<>();
        map.put("code", 1);
        map.put("msg", "登录成功");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));  //返回json数据
    }
}

1.4 登录失败处理

// 失败处理
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {

        Map<String, Object> map = new HashMap<>();

        // 用户名或者秘密错误,在UserDetailsService中,将异常已经抛过来了
        if(exception instanceof InternalAuthenticationServiceException) {
            map.put("code", -2);
            map.put("msg", "内部认证异常");
        }else if(exception instanceof BadCredentialsException) {
            map.put("code", -1);
            map.put("msg", exception.getMessage());
        }
        response.setContentType("applicatoin/json;charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));  //返回json数据
    }
}

1.5 密码加密校验方式

@Component
public class CustomizePasswordEncoder implements PasswordEncoder {

    // 注册的时候使用, 人为的去调用
    @Override
    public String encode(CharSequence rawPassword) {
        return null;
    }

    // 当在返回UserDetails,会自动的去实现校验
    // 第一个参数是传入的密码,  第二个参数是数据库查的密码,
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return false;
    }
}

常用的是框架自带的BCryptPasswordEncoder()

  • BCryptPasswordEncoder()源码
public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN = Pattern
            .compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
    private final Log logger = LogFactory.getLog(getClass());

    private final int strength;

    private final SecureRandom random;

    public BCryptPasswordEncoder() {
        this(-1);
    }

    /**
     * @param strength the log rounds to use, between 4 and 31
     */
    public BCryptPasswordEncoder(int strength) {
        this(strength, null);
    }

    /**
     * @param strength the log rounds to use, between 4 and 31
     * @param random the secure random instance to use
     *
     */
    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
            throw new IllegalArgumentException("Bad strength");
        }
        this.strength = strength;
        this.random = random;
    }

    public String encode(CharSequence rawPassword) {
        String salt;
        if (strength > 0) {
            if (random != null) {
                salt = BCrypt.gensalt(strength, random);
            }
            else {
                salt = BCrypt.gensalt(strength);
            }
        }
        else {
            salt = BCrypt.gensalt();
        }
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.length() == 0) {
            logger.warn("Empty encoded password");
            return false;
        }

        if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
            logger.warn("Encoded password does not look like BCrypt");
            return false;
        }

        return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
    }
}

1.6. 记住我

1.6.1 基本原理

1.6.2 功能实现只需配置几个参数

后面有工程的整体配置, 当前配置仅供参考

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 查询用户数据
    @Autowired
    private UserSecurityService userSecurityService;
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(true);  //true, 自动创建表,
        return jdbcTokenRepository;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 意思是将图片验证码过滤器,加载用户名密码验证过滤器之前
        http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()  //使用form进行登录
                .loginPage("/login.html")   //指定登录页面
                .loginProcessingUrl("/authentication/form")  //表示form往哪里进行提
                //记住我配置
                .rememberMe()
                .tokenValiditySeconds(36000000)
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userSecurityService)    // 因为用户传入过来的token, 需要再次进行校验
                .alwaysRemember(true)
    }
}

1.7 . 图片验证码

1.8 封装验证码类

public class ImageCode implements Serializable{
    private BufferedImage bufferedImage;
    // code是随机字母,需要存储在session中
    private String code;
    // 过期时间
    private LocalDateTime expireTime;
    
    // 第三个参数为过期的时间
    public ImageCode(BufferedImage bufferedImage, String code, int seconds) {
        this.bufferedImage = bufferedImage;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(seconds); //设置过期的时间点
    }
    
    // 验证码是否过期
    public boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);  //当前时间是否在过期时间之后
    }
    
    // setters、getters、other constructors
}

1.9 controller层: 获取验证码图片

@Controller
@RequestMapping("/validate/code")
public class ValidataCodeController {

    // Session的工具类
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public static final String VALIDATE_CODE_KEY = "VALIDATE_CODE_KEY";

    @RequestMapping
    public void validateCode(HttpServletRequest request, HttpServletResponse response) throws IOException {

        ImageCode imageCode = generate(); //生成验证码

        // 将ImageCode存入到session
        sessionStrategy.setAttribute(new ServletWebRequest(request), VALIDATE_CODE_KEY, imageCode);

        //将图片写入前端
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    //生成图片验证码
    private ImageCode generate() {
        int width = 67;
        int height = 30;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        // 随机生成文字
        String sRand = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 22);
        }

        g.dispose();

        return new ImageCode(sRand, image, 60);
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

1.10 增加图片验证码过滤器

/**
 * 一次性的过滤器,过滤之前走,过滤之后不走了
 */
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        if(request.getMethod().equals("POST") && "/authentication/form".equals(request.getRequestURI())) {
            try{
                validateImageCode(request);
            }catch (InternalAuthenticationServiceException exception) {
                myAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception);
                return;
            }
            filterChain.doFilter(request, response);
        }else {
            filterChain.doFilter(request, response);
        }
    }
    // 校验验证码
    private void validateImageCode(HttpServletRequest request) {
        String validateCode = request.getParameter("validateCode"); //获取验证码
        // 取到验证码
        ImageCode imageCode = (ImageCode)sessionStrategy.getAttribute(new ServletWebRequest(request),
                ValidataCodeController.VALIDATE_CODE_KEY);
        // 验证码不能为空或者
        if(StringUtils.isEmpty(validateCode) || "".equals(validateCode.trim())) {
            throw new InternalAuthenticationServiceException("验证不能为空.");
        }
        if(imageCode.isExpire()) {  //是否过期
            throw new InternalAuthenticationServiceException("验证码过期.");
        }
        if(null == imageCode) {
            throw new InternalAuthenticationServiceException("验证码不存在.");
        }
        if(!validateCode.equals(imageCode.getCode())) {
            throw new InternalAuthenticationServiceException("验证码不正确");
        }
    }
}

1.11 全局安全配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 该bean的作用是,在UserDetailsService接口的loadUserByUsername返回的UserDetail中包含了
     * password, 该bean就将用户从页面提交过来的密码进行处理,处理之后与UserDetail中密码进行比较。
     * @return
     */
    // 加载密码加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    // 登录成功执行方法
    @Autowired
    private MySuccessAuthenticationHandler mySuccessAuthenticationHandler;
    //登录失败执行方法
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    //数据库连接池
    @Autowired
    private DataSource dataSource;
    //自定义用户认证
    @Autowired
    private UserSecurityService userSecurityService;
    //验证码过滤器
    @Autowired
    private ImageCodeValidateFilter imageCodeValidateFilter;
    //token仓库
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(true);  //true, 自动创建表,
        return jdbcTokenRepository;
    }
    //全局配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 意思是将图片验证码过滤器,加载用户名密码验证过滤器之前
        http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class) //
            .formLogin()  //使用form进行登录
            .loginPage("/login.html")   //指定登录页面, 前后端分离不用指定
            .loginProcessingUrl("/authentication/form")  //表示form往哪里进行提交
            .successHandler(mySuccessAuthenticationHandler)   //成功后的处理
            .failureHandler(myAuthenticationFailureHandler)   //失败处理
            .and()
            .rememberMe() //记住我
            .tokenValiditySeconds(36000000) //秒
            .tokenRepository(persistentTokenRepository()) //指定token仓库
            .userDetailsService(userSecurityService)    // 因为用户传入过来的token, 需要再次进行校验
            .alwaysRemember(true) //一直记住我
            .and()  //表示进行其他的配置
            .authorizeRequests()   //表示所有的都需要认证
            .antMatchers("/login.html", "/js/**", "/css/**", "/validate/code").permitAll() //意思是让登录页面直接过
            .anyRequest() // 对于所有的请求
            .authenticated()
            .and()
            .csrf().disable(); //
    }
    //此方法用来获取密码令牌进行测试
    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        System.out.println(bCryptPasswordEncoder.encode("1"));
    }
}

如果上述jdbcTokenRepository.setCreateTableOnStartup(false); 需要手动建表,建表语句:

create table persistent_logins (
    username varchar(64) not null,
    series varchar(64) primary key,
    token varchar(64) not null,
    last_used timestamp not null);
举报

相关推荐

spring-security

0 条评论