SpirngBoot整合Spring Security 实现自定义认证

首先,我是一个小白,写的东西有些读者可能看不懂,但是我写博客的目的一个是为了巩固自己学的一些知识,二是当做笔记,三是希望对有需要的人有些帮助。
我这我这里使用了redis+log4j2
不会redis可以去这个网址学习redis,此教程通俗易懂:https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0

  • 在SpringBoot中pom.xml中引入相关的依赖包
        <!--mysql驱动-->
        <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <scope>runtime</scope>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--安全测试-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--redis缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
  • 在application.yml中配置Redis,并自定义RedisTemplate。我使用的是链接(https://www.jianshu.com/p/2909ee88cabb)中的RedisTemplate和RedisUtil工具类;SpringBoot整合Redis可以参考链接:https://www.jianshu.com/p/164ab8815b88
  • 写一个业务层,业务层调用dao层的查询方法,根据用户在前端输入的账号,先去redis缓存中去查,如果缓存中没有,则去数据库查询该用户的账号信息。
  • dao层接口
@Mapper  
public interface UserMapper {
    
    /**
     * 根据用户账号查询用户账号相关的说有信息
     * @param userAccount  用户输入的账号
     * @return 返回的用户信息
     */
    UserAccount queryUserByAccount(String userAccount);
}
  • dao接口对应的Mapper.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.littlenorth.dao.UserMapper">

    <!--根据用户账号查询用户账号相关的说有信息-->
    <select id="queryUserByAccount" resultType="com.littlenorth.dto.UserAccount">
        select Account,Pwd from useraccount where Account=#{userAccount}
    </select>
</mapper>
  • service接口
public interface UserService {
    /**
     * 根据用户账号查询用户账号相关的说有信息
     * @param userAccount
     * @return
     */
    UserAccount queryUserByAccount(String userAccount);
}
  • service接口的实现类
@Slf4j  //使用了log4j2日志
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;  //调用dao层

    @Autowired
    private RedisUtil redisUtil; //调用redis工具类

    /**
     * 根据用户账号查询用户账号相关的说有信息
     * @param userAccount  用户账号
     * @return 返回的用户账号和密码
     */
    @Override
    public UserAccount queryUserByAccount(String userAccount) {
        //判断缓存中是否存在键为user.getAccount()的用户
        UserAccount cacheUserAccount = (UserAccount) redisUtil.get(userAccount);
        if (cacheUserAccount !=null){
            log.info("缓存中有数据!");
            //如果缓存中有,则返回
            return cacheUserAccount;
        }else{
            //如果缓存中没有,则从数据库中进行查询
            UserAccount dataUserAccount = userMapper.queryUserByAccount(userAccount);
            //查询之后将数据缓存 ,缓存一天之后过期
            redisUtil.set(dataUserAccount.getAccount(), dataUserAccount,USER_CACHE_TIME);
            log.info("缓存中没有数据!进数据库查询!");
            return dataUserAccount;
        }
    }
}
  • 写一个UserDetailsService接口的实现类,重写loadUserByUsername方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

@Service  //将类加入到spring容器中
public class UserDetailsServiceImpl implements UserDetailsService{

    @Autowired
    private UserService userService;  //调用业务层

    /**
     * 重新父类方法
     * @param userAccount  用户账号
     * @return spring security自动封装之后的用户
     * @throws UsernameNotFoundException 抛出异常
     */
    @Override
    public UserDetails loadUserByUsername(String userAccount) throws UsernameNotFoundException {

        //获取用户账号信息
        UserAccount user = userService.queryUserByAccount(userAccount);
        //存储角色集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        //添加like角色,角色必须以`ROLE_`开头,数据库中没有,则在这里加。
        authorities.add(new SimpleGrantedAuthority("ROLE_like")); //like不是从数据库中查询出的角色
        if (user!=null){
            return new User(user.getAccount(),user.getPwd(),authorities);
        }else{
            throw new UsernameNotFoundException("当前用户名不存在!");
        }
    }
}
  • 自定义登录成功处理类
import com.alibaba.fastjson.JSONObject;
import com.littlenorth.common.AjaxResult;
import com.littlenorth.common.Constants;
import com.littlenorth.constant.HttpStatus;
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;


/**
 * 用户登录成功处理类
 */
@Component(value = "userAuthenticationSuccessHandler")
public class UserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //自定义返回消息
        AjaxResult result = new AjaxResult(HttpStatus.SUCCESS,Constants.LOGIN_SUCCESS);
        //设置响应头
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSONObject.toJSONString(result));
    }
}
  • 自定义登录失败处理类
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义登录失败
 */
@Component
public class UserAuthenticationFailureHandler  implements AuthenticationFailureHandler{

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        AjaxResult result = null;
        if(e instanceof UsernameNotFoundException){
            result=AjaxResult.error(e.getMessage(),Constants.DATA);
        }else if(e instanceof BadCredentialsException){
            result=AjaxResult.error(Constants.LOGIN_PWD_ERROR,Constants.DATA);
        }else {
            result=AjaxResult.error(e.getMessage());
        }
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSONObject.toJSONString(result));
    }
}
  • 自定义未登录处理类
import com.alibaba.fastjson.JSONObject;
import com.littlenorth.common.AjaxResult;
import com.littlenorth.common.Constants;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * 自定义未登录的处理逻辑
 */
@Component
public class UserAuthenticationEnryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSONObject.toJSONString(new AjaxResult(Constants.REQUIRE_LOGIN)));

    }
}
  • 自定义无权限处理类
import com.alibaba.fastjson.JSONObject;
import com.littlenorth.common.AjaxResult;
import com.littlenorth.common.Constants;
import com.littlenorth.constant.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * 自定义无权限访问时的逻辑处理
 */
@Component
public class UserAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSONObject.toJSONString(new AjaxResult(HttpStatus.FORBIDDEN,Constants.NO_POWER)));
    }
}
  • 自定义验证逻辑处理类
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.security.core.userdetails.UserDetails;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


/**
 * 自定义认证逻辑处理
 */
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //获取用户名
        String account= authentication.getName();
        // //获取密码
        String password= (String) authentication.getCredentials();
        UserDetails userDetails= userDetailsService.loadUserByUsername(account);
        boolean checkPassword= passwordEncoder.matches(password,userDetails.getPassword());
        if(!checkPassword){
            throw new BadCredentialsException("密码不正确,请重新登录!");
        }
        return new UsernamePasswordAuthenticationToken(account,password,userDetails.getAuthorities());

    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}
  • 创建SecurityConfig继承WebSecurityConfigurerAdapter,并重写configure(AuthenticationManagerBuilder auth)方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    //注入自定义登录成功处理类
    @Autowired
    private UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;


    //注入自定义失败处理类
    @Autowired
    private UserAuthenticationFailureHandler authenticationFailureHandler;


    //注入自定义未登录处理类
    @Autowired
    private UserAuthenticationEnryPoint userAuthenticationEnryPoint;

    //无权访问
    @Autowired
    private UserAccessDeniedHandler accessDeniedHandler;

    //未登录
    @Autowired
    private UserAuthenticationEnryPoint authenticationEnryPoint;

    //自定义认证逻辑处理
    @Autowired
    private SelfAuthenticationProvider selfAuthenticationProvider;

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


    /**
     * 用户认证
     * @param auth  用户认证
     * @throws Exception  异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(selfAuthenticationProvider);
    }


    /**
     * 用户访问控制
     * @param http  请求
     * @throws Exception 抛出异常
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // /user的所有请求都要有like权限,否则去登录页面
        http.authorizeRequests()
                .antMatchers("/dologin").permitAll()
                .antMatchers("/user/**").authenticated()  //"/login"不进行权限验证
                //.anyRequest().authenticated()   //其他的需要登陆后才能访问
                .and()
                .formLogin()
                .loginPage("/login.html") //自定义登录页面
                .loginProcessingUrl("/dologin")  //登录的请求
                .successHandler(userAuthenticationSuccessHandler)  //登录成功之后的处理
                .failureHandler(authenticationFailureHandler)     //登录失败的处理
                .and()
                .csrf().disable()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)    //权限不足的时候的逻辑处理
                .authenticationEntryPoint(authenticationEnryPoint);  //未登录是的逻辑处理


    }
}

此外文章中用到的常量和工具类

/**
 * 通用常量信息
 */

public class Constants {
    /**
     * 登录成功
     */
    public static final String LOGIN_SUCCESS = "登录成功!";
    /**
     * 登录失败
     */
    public static final String LOGIN_PWD_ERROR = "密码错误!";

    /**
     * 返回数据
     */
    public static final String DATA=null;

    /**
     * 未登录
     */
    public static final String REQUIRE_LOGIN="请先登录!";

    /**
     * 无权限
     */
    public static final String NO_POWER="没有权限!";
}

```java
import java.util.HashMap;

/**
 * 操作消息提醒类
 */
public class AjaxResult extends HashMap<String, Object> {
    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     */
    public AjaxResult(int code, String msg)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }


    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param msg 返回内容
     */
    public AjaxResult(String msg){
        super.put(MSG_TAG,msg);
    }


    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (StringUtils.isNotNull(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(int code, String msg)
    {
        return new AjaxResult(code, msg, null);
    }

}
```java
/**
 * 返回的状态码
 */
public class HttpStatus {
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    public static final int NO_CONTENT = 204;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    public static final int BAD_METHOD = 405;

    public static final int CONFLICT = 409;
    
    public static final int UNSUPPORTED_TYPE = 415;
   
    public static final int ERROR = 500;

 
}

注意:我数据库中用户的密码存的是密文,即BCryptPasswordEncoder加密之后的密码。不懂的可以参考此链接的博客:https://blog.csdn.net/bookssea/article/details/109262109
看不懂可以参考更详细的Spring Security的文章:https://blog.csdn.net/weixin_42375707/article/details/110678638

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

推荐阅读更多精彩内容