毫无存在的沉默之路——初窥spring security,springboot集成spring security

1 spring boot集成spring security

1.1 初始化工程

数据库

创建一个 t_member 表存储用户信息

DROP TABLE IF EXISTS `t_member`;
CREATE TABLE `t_member`  (
  `id` int 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 (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;


INSERT INTO `t_member` (`id`, `username`, `password`) VALUES (1, 'admin', '123456');
INSERT INTO `t_member` (`id`, `username`, `password`) VALUES (2, 'yuwen', '123456');
INSERT INTO `t_member` (`id`, `username`, `password`) VALUES (3, 'yulei', '123456');

pom.xml

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

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

        <!--mybatis-spring-boot-starter-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://10.19.11.107:3306/demo-all?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  main:
    allow-bean-definition-overriding: true

mybatis:
  type-aliases-package: com.example.demooauth2authorization.dao
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: classpath:mapper/*.xml

logging:
  level:
    root: info
    com.example.demooauth2authorization.dao: debug

MemberController.java

@RestController
public class MemberController {

    @Autowired
    private MemberDao memberDao;

    /**
     * 查询所有用户
     *
     * @return List<User>
     */
    @GetMapping("getAll")
    public List<Member> getAll() {
        return memberDao.getAll();
    }

}

MemberDao.java

public interface MemberDao {

    /**
     * 查询所有用户
     *
     * @return List<User>
     */
    List<Member> getAll();
}

Member.java 

@Data
public class Member {

    private Integer id;

    private String username;

    private String password;
}

MemberDao.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" >
<!-- 映射文件,映射到对应的SQL接口 -->
<mapper namespace="com.example.demooauth2authorization.dao.MemberDao">

    <!--查询所有用户-->
    <select id="getAll" resultType="com.example.demooauth2authorization.entity.Member">
        select *
        from t_Member
    </select>

    <!--根据用户名获取用户-->
    <select id="getUser" resultType="com.example.demooauth2authorization.entity.Member">
        select *
        from t_Member
        where username = #{username}
    </select>

</mapper>

hah,没有service层,因为这是一个demo,怎么简单怎么来。

启动工程,访问:http://127.0.0.1:8080/getAll

可以看到数据正常展示。

1.2 集成spring security

加入依赖:

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

新建类实现UserDetailService接口重写loadUserByUsername方法。

 CustomUserDetailsServiceImpl.java

package com.example.demooauth2authorization.security;

import com.example.demooauth2authorization.dao.MemberDao;
import com.example.demooauth2authorization.entity.Member;
import com.example.demooauth2authorization.util.PasswordEncoderUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 相柳
 * @date 2021/9/4
 */
@Component
@Slf4j
public class CustomUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private MemberDao memberDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("调用loadUserByUsername");
        // 通过用户名从数据库获取用户信息
        Member member = memberDao.getUser(username);
        if (member == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 角色集合
        List<GrantedAuthority> roles = new ArrayList<>();
        return new User(
                member.getUsername(),
                // 因为数据库是明文,所以这里需加密密码
                PasswordEncoderUtil.encode(member.getPassword()),
                roles
        );
    }

}

PasswordEncoderUtil.java

package com.example.demooauth2authorization.util;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * @author 相柳
 * @date 2021/9/6
 */
public class PasswordEncoderUtil {
    public static Boolean matches(String raw, String encoded) {
        return new BCryptPasswordEncoder().matches(raw, encoded);
    }

    public static String encode(String raw) {
        return new BCryptPasswordEncoder().encode(raw);
    }
}

创建Security的配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,并重写configure(auth)方法。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsServiceImpl customUserDetailsService;

    /**
     * 指定加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt加密密码
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                // 从数据库读取的用户进行身份认证
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }
}

重启工程,再次访问:http://127.0.0.1:8080/getAll

出现如下登录界面,这是spring security默认的登录页。输入数据库中的用户信息,登录进入即可

 2 spring security的配置

2.1 自定义登录页

使用freemarker作为模板,加入依赖:

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

然后把登录界面和欢迎页面写好,

login.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">

    <title>微服务统一认证</title>

    <link href="/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/signin.css" rel="stylesheet">
</head>

<body class="sign_body">
<div class="container form-margin-top">
    <form class="form-signin" action="/member/login" method="post" style="width: 300px;margin: 0px auto;">
        <h2 class="form-signin-heading" align="center">自定义统一认证系统</h2>
        <input type="text" name="username" class="form-control form-margin-top" placeholder="账号" required autofocus><br/>
        <input type="password" name="password" class="form-control" placeholder="密码" required><br/>
        <button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button><br/>
        <#if error??>
            <span style="color: red; ">${error}</span>
        </#if>
    </form>
</div>
</body>
</html>

welcome.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">

    <title>微服务统一认证</title>

    <link href="/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/signin.css" rel="stylesheet">
</head>

<body class="sign_body">
<div class="container form-margin-top">
    尊敬的:<span style="color: red">
        <#if username??>
            ${username}
        </#if>
    </span>,欢迎您
</div>
</body>
</html>

自定义 LoginValidProvider 继承自 AuthenticationProvider。实现认证逻辑

LoginValidProvider.java

package com.example.demooauth2authorization.security;

import com.example.demooauth2authorization.dao.MemberDao;
import com.example.demooauth2authorization.entity.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

/**
 * @author 相柳
 * @date 2021/9/6
 */

@Component
@Slf4j
public class LoginValidProvider implements AuthenticationProvider {

    @Autowired
    private MemberDao memberDao;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        log.info("身份认证开始工作");
        String username = authentication.getName();
        String rawPassword = (String) authentication.getCredentials();

        // 数据库中查询用户
        Member member = memberDao.selectByUnameAndUPass(username, rawPassword);
        if (member != null) {
            return new UsernamePasswordAuthenticationToken(member, rawPassword, new ArrayList<>());
        } else {
            throw new BadCredentialsException("用户名或者密码错误");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

然后自定义认证成功、认证失败和登出的处理函数(这是可选的)

LoginSuccessHandle.java

package com.example.demooauth2authorization.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 相柳
 * @date 2021/9/6
 */
@Slf4j
@Component
public class LoginSuccessHandle implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("进入登录成功handle");
        response.sendRedirect("/member/toWelcome");
    }
}

LoginFailHandle.java

package com.example.demooauth2authorization.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;

/**
 * @author 相柳
 * @date 2021/9/7
 */
@Slf4j
@Component
public class LoginFailHandle implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("进入登录失败handle");
        String toLogin = "/member/toLogin?error=";
        String url = toLogin + URLEncoder.encode(exception.getMessage(), "UTF-8");
        response.sendRedirect(url);
    }

}

LogoutHandle.java

package com.example.demooauth2authorization.security;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 相柳
 * @date 2021/9/7
 */
@Slf4j
@Component
public class LogoutHandle implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登出handle");
        // 获取请求参数中是否包含回调地址
        String redirectUrl = request.getParameter("redirect_url");
        if (StringUtils.isNotBlank(redirectUrl)) {
            response.sendRedirect(redirectUrl);
        }
        else {
            // 默认跳转referer 地址
            String referer = request.getHeader(HttpHeaders.REFERER);
            response.sendRedirect(referer);
        }
    }
}

接下来配置 WebSecurityConfig.java 。这是最重要的

package com.example.demooauth2authorization.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author 相柳
 * @date 2021/9/4
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private LoginValidProvider loginValidProvider;

    @Autowired
    private LoginSuccessHandle loginSuccessHandle;

    @Autowired
    private LoginFailHandle loginFailHandle;

    @Autowired
    private LogoutHandle logoutHandle;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * 指定加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt加密密码
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        String toLogin = "/member/toLogin";
        String checkPath = "/member/login";
        http
                // 自定义登录页
                .formLogin()
                // 登录表单提交的url,即和form的action一致
                .loginProcessingUrl(checkPath)
                // 访问登录页的url
                .loginPage(toLogin)

                // 设置登录成功和失败的处理器
                .successHandler(loginSuccessHandle)
                .failureHandler(loginFailHandle)

                // 设置登出操作
                .and().logout()
                .logoutSuccessHandler(logoutHandle)
                .deleteCookies("JSESSIONID").invalidateHttpSession(true)

                // 登录请求不需要认证
                .and().authorizeRequests().antMatchers(toLogin).permitAll()

                // 其他的所有请求都需要认证
                .anyRequest().authenticated()

                // 关闭csrf防护
                .and().csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置自定义认证
        auth.authenticationProvider(loginValidProvider);
    }
}

2.2 登录成功自动跳转到登录前的地址

改造之前的 LoginSuccesshandle.java

package com.example.demooauth2authorization.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author 相柳
 * @date 2021/9/6
 */
@Slf4j
@Component
public class LoginSuccessHandle implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("进入登录成功handle");
        RequestCache requestCache = new HttpSessionRequestCache();
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest == null) {
            response.sendRedirect("/member/toWelcome");
            return;
        }
        // 返回之前请求的地址
        clearAuthenticationAttributes(request);
        String targetUrl = savedRequest.getRedirectUrl();
        response.sendRedirect(targetUrl);
    }

    protected final void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        }
    }
}

这段代码是仿照 spring security 内置的 SavedRequestAwareAuthenticationSuccessHandler 所写。SavedRequestAwareAuthenticationSuccessHandler 也有在登录后返回原来请求地址的作用。所以登录成功的处理逻辑也可以去继承它来实现跳转到之前的页面

--------------------------------------------------------

这篇博客是我看博客,看文档,看源码,然后结合一个开源项目和自身4年开发经验写出来的,仅供参考,留以备忘。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值