Spring——security安全框架使用详解(基于内存认证)

Spring——security安全框架使用详解

1.前言

在日常开发中,几乎所有的项目都需要进行请求的安全校验操作。

通常会采取以下几种方式来实现安全校验过滤

  • 1.实例化HandlerInterceptor接口,配置其中的preHandlepostHandleafterCompletion 属性信息。
  • 2、采取AOP的思想,手写一个接口的拦截过滤。
  • 3、使用一些较为成熟的权限认证、校验框架。如:shiro、security等。

2.Security简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以多么容易地扩展以满足定制需求。

它是Spring家族中的一个安全管理框架。相比于另一个安全框架shiro,它提供了更丰富的功能,社区资源也比shiro丰富

官方文档地址

https://spring.io/projects/spring-security/

3.Security相关模块

  • 核心模块:Spring-security-core.jar。包含核心验证和访问控制类接口,远程支持的基本配置API。任何使用Spring Security 的应用程序都需要这个模块。支持独立的应用程序、远程客户端、服务层方法安全和jdbc用户配置。

    包含以下顶层包:

    • org.springframework.security.core
    • org.springframework.security.access
    • org.springframework.security.authentication
    • org.springframework.security.provisioning
  • 远程调用:spring-security-remoting.jar。提供与 Spring Remoting 集成。
    主要包为:org.springframework.security.remoting

  • 网页:spring-security-web.jar。包括网站安全的模块,提供网站认证服务和基于 URL 访问控制。
    主包名为 org.springframework.security.web

  • 配置:spring-security-config.jar。包含安全命令空间的解析代码。如果你使用Spring Security XML命令空间进行配置你需要包含这个模块。 主包名为org.springframework.security.config

  • LDAP:spring-security-ldap.jar。LDAP验证和配置代码,如果你需要使用LDAP验证和管理LDAP用户实体,你需要这个模块。
    主包名为 org.springframework.security.ldap

  • ACL访问控制表:spring-security-acl.jar。ACL专门的领域对象的实现。用来在你的应用程序中应用安全特定的领域对象实例。
    主包名为 org.springframework.security.acls

  • CAS:spring-security-cas.jar。Spring Security的CAS客户端集成。如果你想用CAS的SSO服务器使用Spring Security网页验证需要该模块。
    顶层的包是 org.springframework.security.cas

  • OpenID:spring-security-openid.jar。OpenID 网页验证支持。使用外部的OpenID服务器验证用户。 org.springframework.security.openid. 需要 OpenID4Java

  • Test:spring-security-test.jar。支持Spring Security的测试。

4.快速入门

  • 创建一个SpringBoot项目
 	   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- springboot 快速启动的一些基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
       <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
  • 导入SpringSecurity依赖
<!--        security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

版本号问题:一些SpringBoot整合了一些通用依赖的版本号,所以不需要自己手动控制依赖的版本号信息

  • 创建一个Controller进行测试

运行项目会跳转到security提供的一个登录页面(用户名:user)密码在控制台中查看
在这里插入图片描述
密码(运行好项目后会打印在控制台):
在这里插入图片描述

更改用户名和密码,在application.yml中去设置security的username和password

Spring:
  security:
    user:
      password: 123456
      name: root

5.实际项目场景分析

  1. 实际项目中不会使用它默认的登录页(删除)
  2. 登录验证需要结合数据库进行校验
  3. 登录成功需要返回一个jwtToken

登录时

  1. ​ 自定义登录接口

     调用ProviderManager的方法进行认证,如果认证通过生成jwt
    

    ​ 把用户信息存入redis中

  2. 自定义UserDetailService

     在这个实现类中去查询数据库
    

校验

  1. 定义jwt认证过滤器

    ​ 获取Token,解析Token,获取其中的key

    ​ 通过key查询存入到redis中的用户信息

    ​ 存入SecurityContentHolder
    在这里插入图片描述

6.SpringSecurity完整流程

SpringSecurity的原理就是一个过滤器链,内部包含了各种功能的过滤器。它其中包含最主要的过滤器有以下这些:

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

  • ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

  • FilterSecurityInterceptor: 负责权限校验的过滤器。

在这里插入图片描述

我们需要实现的接口:

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

  • AuthenticationManager接口:定义了认证Authentication的方法

  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中

7.实现步骤

7.1基础登录校验

  1. 添加依赖

            <!--redis依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!--jwt依赖-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
    
    
  2. 添加redis相关配置

    @Configuration
    public class RedisConfig {
    
        @Resource
        private RedisConnectionFactory factory;
        @Bean
        public RedisTemplate redisTemplate(){
            RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
            //连接redis,设置连接工厂
            redisTemplate.setConnectionFactory(factory);
            // 处理键值对数据序列化,不处理序列化会数据会乱码
            // 针对key 和值都做序列化处理
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
    
            // 特殊格式数据处理
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            om.setTimeZone(TimeZone.getDefault());
            om.configure(MapperFeature.USE_ANNOTATIONS, false);
            om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    
            return redisTemplate;
        }
    
  3. 响应类

    /**
     * 接口通用的返回对象。
     *
     * @author zyy
     */
    public final class R<T> {
        private int code;
        private String msg;
        private T data;
    
        public R() {
    
        }
    
        public R(int code) {
            this.code = code;
            this.msg = "";
            this.data = null;
        }
    
        public R(int code, String msg) {
            this.code = code;
            this.msg = msg;
            this.data = null;
        }
    
        public R(int code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    
        public static R Success(Object data) {
            return new R(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMessage(), data);
        }
    
        public static R Success(String message, Object data) {
            return new R(ResultCodeEnum.SUCCESS.getCode(), message, data);
        }
    
        public static R Success() {
            return Success("");
        }
    
        public static R Failed(String msg) {
            return new R(ResultCodeEnum.SYSTEM_EXCEPTION.getCode(), msg);
        }
    
        public static R Failed() {
            return Failed("Failed");
        }
    
        public static R Failed(int code, String msg) {
            return new R(code, msg);
        }
    
        public int getCode() {
            return code;
        }
    
        public void setCode(int code) {
            this.code = code;
        }
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    
        public boolean succeeded() {
            return getCode() == ResultCodeEnum.SUCCESS.getCode();
        }
    
    
        public boolean failed() {
            return getCode() != ResultCodeEnum.SUCCESS.getCode();
        }
    
    }
    
  4. jwtTokenUtil工具类

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTDecodeException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    public class JwtTokenUtil {
    
        // 创建一个秘钥
        public static final String TOKEN_KEY = "ZhengYY";
    
        // 设置token的有效期  15min
        public final static long KEEP_TIME = 15 * 60 * 60 * 1000;
    
        /**
         * 生成token
         *
         * @param account 用户名
         * @param accName
         * @return token
         */
        public static String buildJwt(String account, String accName) {
            Date date = new Date(System.currentTimeMillis() + KEEP_TIME);
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY);
            // 设置头部信息
            Map header = new HashMap<>(2);
            header.put("typ", "JWT");
            header.put("alg", "HS256");
            return JWT.create()
                    .withHeader(header)
                    .withClaim("account", account)//设置自定义内容
                    .withClaim("accName", accName)
                    .withExpiresAt(date)//设置有效期
                    .sign(algorithm);
        }
    
    
        /**
         * 校验token是否正确
         *
         * @param token
         * @return
         */
        public static boolean verify(String token) {
            try {
                Algorithm algorithm = Algorithm.HMAC256(TOKEN_KEY);
                JWTVerifier verifier = JWT.require(algorithm)
                        .build();
                DecodedJWT jwt = verifier.verify(token);
                return true;
            } catch (Exception exception) {
                return false;
            }
        }
    
    
        /**
         * 获得token中的信息无需secret解密也能获得
         *
         * @return token中包含的用户名
         */
        public static String getAccName(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("accName").asString();
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    
        /**
         * 获取登陆用户账号
         *
         * @param token
         * @return
         */
        public static String getAccount(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("account").asString();
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    }
    
    
  5. 使用SpringSecurity自带的密码校验格式校验用户

    我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。

    5.1:准备数据库

    /*
     Navicat Premium Data Transfer
    
     Source Server         : localhost_3306
     Source Server Type    : MySQL
     Source Server Version : 80034
     Source Host           : localhost:3306
     Source Schema         : smart_from
    
     Target Server Type    : MySQL
     Target Server Version : 80034
     File Encoding         : 65001
    
     Date: 03/01/2024 11:43:33
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for account_info
    -- ----------------------------
    DROP TABLE IF EXISTS `account_info`;
    CREATE TABLE `account_info`  (
      `account` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号,主键',
      `acc_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户姓名',
      `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
      `acc_phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号11位,唯一',
      `is_enable` tinyint(1) NOT NULL COMMENT '是否启用(1:启用,0:未启用)',
      `create_time` datetime NOT NULL COMMENT '创建时间',
      `update_time` datetime NOT NULL COMMENT '更新时间',
      PRIMARY KEY (`account`) USING BTREE,
      UNIQUE INDEX `uk_phone`(`acc_phone` ASC) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    

    5.2:准备实体类

    @Data
    @EqualsAndHashCode(callSuper = false)
    public class AccountInfo implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        /**
         * 用户账号,主键
         */
        private String account;
    
        /**
         * 用户姓名
         */
        private String accName;
    
        /**
         * 鐢ㄦ埛瀵嗙爜锛岄粯璁ゆ墜鏈哄彿鍚?浣?
         */
        private String password;
    
        /**
         * 手机号11位,唯一
         */
        private String accPhone;
    
        /**
         * 是否启用(1:启用,0:未启用)
         */
        private Boolean isEnable;
    
        /**
         * 创建时间
         */
        private Date createTime;
    
        /**
         * 更新时间
         */
        private Date updateTime;
    

    5.3:因为UserDetailsService方法的返回值是UserDetails类型,所以我们需要定义一个类实现UserDetails接口,把用户信息封装到里面

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginAccountInfo implements UserDetails,Serializable{
    
        private AccountInfo accountInfo;
    
        @Override
        public String getPassword() {
            return accountInfo.getPassword();
        }
    
        @Override
        public String getUsername() {
            return accountInfo.getAccount();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    

    5.4:创建一个类实现UserDetailsService接口,重写其中的方法。增加用户名从数据库中查询用户信息

    @Service
    public class AccountInfoServiceImpl extends ServiceImpl<AccountInfoMapper, AccountInfo> implements AccountInfoService, UserDetailsService {
        @Autowired
        private AccountInfoMapper accountInfoMapper;
    	 /**
         * 重写Userdetails中的loadUserByUsername方法去查询数据库中的
         * @param account
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
            //查询用户信息
            LambdaQueryWrapper<AccountInfo> lqw = new LambdaQueryWrapper<>();
            lqw.eq(AccountInfo::getAccount,account);
            AccountInfo accountInfo = accountInfoMapper.selectOne(lqw);
            System.out.println(accountInfo);
            //没有查询到用户就抛出异常
            if (ObjectUtils.isEmpty(accountInfo)){
                throw new RuntimeException("账号获取密码错误");
            }
            //TODO 查询对应的权限信息
    
            //把数据封装成UserDetails
            return new LoginAccountInfo(accountInfo);
        }
    
    
    }
    

    注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop} 例如:

在这里插入图片描述

​ 这样登录就可以使用YZ0002这个账号,123456作为密码登录

7.2使用Md5加密存储和校验

  1. 配置Md5Utils工具类,这个配置类要去实现SpringScurity提供的接口,并且重写它的转换密码和比较密码的两个方法

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    import java.security.MessageDigest;
    
    /**
     * @ProjectName: Md5Utils.java
     * @Package: net.zlr.fengine.utils
     * @ClassName Md5Utils
     * @Author pengguo
     * @Description MD5加密
     * @Date 11:36 2020/1/2
     * @version v1.0.0
     */
    public class Md5Utils implements PasswordEncoder {
        private static final Logger log = LoggerFactory.getLogger(Md5Utils.class);
    
        private static byte[] md5(String s)
        {
            MessageDigest algorithm;
            try
            {
                algorithm = MessageDigest.getInstance("MD5");
                algorithm.reset();
                algorithm.update(s.getBytes("UTF-8"));
                byte[] messageDigest = algorithm.digest();
                return messageDigest;
            }
            catch (Exception e)
            {
                log.error("MD5 Error...", e);
            }
            return null;
        }
    
        private static final String toHex(byte hash[])
        {
            if (hash == null)
            {
                return null;
            }
            StringBuffer buf = new StringBuffer(hash.length * 2);
            int i;
    
            for (i = 0; i < hash.length; i++)
            {
                if ((hash[i] & 0xff) < 0x10)
                {
                    buf.append("0");
                }
                buf.append(Long.toString(hash[i] & 0xff, 16));
            }
            return buf.toString();
        }
    
        public static String hash(String s)
        {
            try
            {
                return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8");
            }
            catch (Exception e)
            {
                log.error("not supported charset...{}", e);
                return s;
            }
        }
    
        public String encode(CharSequence rawPassword) {
            //将rawPassword转换成MD5加密后的字符串
            return hash(rawPassword.toString());
        }
    
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            //将rawPasswords是需要加密的密码和encodePassword是存入数据库的密码
            String input = hash(rawPassword.toString());
            return input.equals(encodedPassword.toString());
        }
    }
    
  2. 定义一个SpringSecurity配置类,这个配置类继承WebSecurityConfigurerAdpter

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private AccountInfoServiceImpl accountInfoService;
    
        @Autowired
        private JwtLoginTokenFilter jwtLoginTokenFilter;
    
        //创建BCryptPasswordEncoder注入容器
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new Md5Utils();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http    //关闭csrf
                    .csrf().disable()
                    //不通过session获取SecurityContent
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    //对于登录接口,允许匿名访问
                    .antMatchers("/accountInfo/login").anonymous()//permitAll()方法,无论有没有登录都可以访问
                    //除上面外的所有请求全部需要鉴证认证
                    .anyRequest().authenticated();
        }
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 配置认证方式,使用自定义的密码编码器
            auth.userDetailsService(accountInfoService);
        }
    
    }
    

    configure(AuthenticationManagerBuilder auth)方法的作用是通过设置UserDetailsService来指定如何获取用户的认证信息,以便在用户登录时进行认证。这样,Spring Security就能够根据你提供的认证方式,对用户进行身份验证和授权操作。

  3. 编写登录接口

        @Autowired
        private AccountInfoService accountInfoService;
        //登录
        @ApiOperation("用户登录")
        @PostMapping("/login")
        public  R<Map<String,String>> accountLogin(@RequestBody AccountInfo accountInfo){
            System.out.println(accountInfo);
            //登录
            return accountInfoService.login(accountInfo);
        }
    

    在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

    认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

    	@Resource
        private RedisTemplate redisTemplate;
        @Autowired
        private AccountInfoMapper accountInfoMapper;
        @Autowired
        private AuthenticationManager authenticationManager;
    	/**
         * 使用Security的登录
         * @param accountInfo
         * @return
         */
        public R<Map<String, String>> login(AccountInfo accountInfo) {
            //AuthenticationManager authenticate进行用户认证
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo.getAccount(),accountInfo.getPassword());
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
            //如果认证没通过给出对应的提升
            if(ObjectUtils.isEmpty(authenticate)){
                throw new RuntimeException("登录失败");
            }
            //如果认证通过,使用userid生成一个jwt
            LoginAccountInfo loginAccountInfo = (LoginAccountInfo) authenticate.getPrincipal();
            AccountInfo accountInfo1 = loginAccountInfo.getAccountInfo();
            String account = accountInfo1.getAccount();
            String accName = accountInfo1.getAccName();
            String token = JwtTokenUtil.buildJwt(account, accName);
            Map<String,String> map = new HashMap<>();
            map.put("token",token);
            //把完整的用户信息存入redis  userid作为key
            redisTemplate.opsForValue().set(account,loginAccountInfo);
            return R.Success(map);
        }
    

    这样配置之后就可以用一个使用md5加密后的账号进行测试

7.3认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。

使用userid去redis中获取对应的LoginUser对象。

然后封装Authentication对象存入SecurityContextHolder

@Component
public class JwtLoginTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取Token
        String token = request.getHeader("token");
        if(token==null){
            //放行
            filterChain.doFilter(request,response);
            return;
        }
        //解析Token
        String account = JwtTokenUtil.getAccount(token);
        //从redis中获取用户信息
        String redisKey=account;
        LoginAccountInfo accountInfo = (LoginAccountInfo) redisTemplate.opsForValue().get(redisKey);
        System.out.println(accountInfo);
        if (ObjectUtils.isEmpty(accountInfo)){
            //用户未登录
            R result = R.Failed("用户未登录");
            String str = JSONObject.toJSONString(result);
            response.setCharacterEncoding("utf-8");
            response.getWriter().write(str);
            return;
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,null,accountInfo.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AccountInfoServiceImpl accountInfoService;

    @Autowired
    private JwtLoginTokenFilter jwtLoginTokenFilter;

    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new Md5Utils();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    //关闭csrf
                .csrf().disable()
                //不通过session获取SecurityContent
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //对于登录接口,允许匿名访问
                .antMatchers("/accountInfo/login").anonymous()//permitAll()方法,无论有没有登录都可以访问
                //除上面外的所有请求全部需要鉴证认证
                .anyRequest().authenticated();
        //添加Token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtLoginTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置认证方式,使用自定义的密码编码器
        auth.userDetailsService(accountInfoService);
    }

}

7.4退出登录

退出登录逻辑很简单:我们只需要定义一个退出登录的接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

    /**
     * 退出登录
     * @return
     */
    @Override
    public R logout() {
        //获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginAccountInfo accountInfo = (LoginAccountInfo) authentication.getPrincipal();
        String account = accountInfo.getAccountInfo().getAccount();
        //删除redis中的值
        redisTemplate.delete(account);
        return R.Success("退出成功");
    }

7.5权限设置和校验

7.5.1限制接口访问权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

@EnableGlobalMethodSecurity(prePostEnabled = true)

这个加到SpringSecurity的配置类上面。

找到需要进行权限校验的接口,在方法上面加上@PreAuthoriza注解。

编写一个查询所有用户的接口

    /**
     * 查询所有用户的信息
     * @return
     */
    @PostMapping("/page")
    @PreAuthorize("hasAnyAuthority('test')")
    public R queryAccount(){
        return accountInfoService.queryAccount();
    }
    /**
     * 查询所有用户信息
     **/
    @Override
    public R queryAccount() {
        List<AccountInfo> accountInfos = accountInfoMapper.selectList(null);
        return R.Success(accountInfos);
    }

@PreAuthorize("hasAuthority('test')")是Spring Security提供的注解之一,它的作用是在方法调用之前进行权限验证。

7.5.2封装权限信息

在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。

我们先直接把权限信息写死封装到UserDetails中进行测试。

我们之前定义了UserDetails的实现类LoginAccountInfo,想要让其能封装权限信息就要对其进行修改。

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginAccountInfo implements UserDetails,Serializable{

    private AccountInfo accountInfo;
    private List<String> permissions;

    public LoginAccountInfo(AccountInfo accountInfo, List<String> permissions) {
        this.accountInfo = accountInfo;
        this.permissions = permissions;
    }
    //存储SpringSecurity所需要的权限信息的集合
    @JsonIgnore
    private List<SimpleGrantedAuthority> authorities;


    /**
     * 获取权限信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permission中的string类型的权限信息封装成SimpleGrantedAuthority对象
//       List<SimpleGrantedAuthority> newList = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
//            newList.add(authority);
//        }
        //使用函数编程简化
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return accountInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return accountInfo.getAccount();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

LoginAccountInfo修改完后我们就可以在AccountInfoServiceImpl中去把权限信息封装到LoginAccountInfo中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。

    /**
     * 重写Userdetails中的loadUserByUsername方法去查询数据库中的
     * @param account
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        //查询用户信息
        LambdaQueryWrapper<AccountInfo> lqw = new LambdaQueryWrapper<>();
        lqw.eq(AccountInfo::getAccount,account);
        AccountInfo accountInfo = accountInfoMapper.selectOne(lqw);
        System.out.println(accountInfo);
        //没有查询到用户就抛出异常
        if (ObjectUtils.isEmpty(accountInfo)){
            throw new RuntimeException("账号获取密码错误");
        }
        //TODO 查询对应的权限信息
        List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
        //把数据封装成UserDetails
        return new LoginAccountInfo(accountInfo,list);
    }

校验的时候从redis中获取权限

@Component
public class JwtLoginTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取Token
        String token = request.getHeader("token");
        if(token==null){
            //放行
            filterChain.doFilter(request,response);
            return;
        }
        //解析Token
        String account = JwtTokenUtil.getAccount(token);
        //从redis中获取用户信息
        String redisKey=account;
        LoginAccountInfo accountInfo = (LoginAccountInfo) redisTemplate.opsForValue().get(redisKey);
        System.out.println(accountInfo);
        if (ObjectUtils.isEmpty(accountInfo)){
            //用户未登录
            R result = R.Failed("用户未登录");
            String str = JSONObject.toJSONString(result);
            response.setCharacterEncoding("utf-8");
            response.getWriter().write(str);
//            throw new RuntimeException("用户未登录!");
            return;
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,null,accountInfo.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
}

测试权限校验,更改接口所需要的权限

  @PreAuthorize("hasAnyAuthority('test1')")

在这里插入图片描述
出现不允许访问就测试成功
在这里插入图片描述
出现Bad credentials 则表示用户名或密码错误

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security是一个强大的安全框架,它提供了很多功能来保护应用程序免受各种攻击。下面是使用Spring Security的基本步骤: 1. 添加Spring Security依赖 在Maven项目中,在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.3.3.RELEASE</version> </dependency> ``` 2. 配置Spring Security 创建一个Spring Security配置类,以配置身份验证和授权规则。可以使用以下注释来启用Spring Security: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // 配置 } ``` 然后,可以覆盖configure()方法来定义身份验证和授权规则: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") .antMatchers("/**").permitAll() .and().formLogin(); } ``` 上面的代码定义了如下规则: - /admin/**路径需要ADMIN角色才能访问 - /user/**路径需要USER或ADMIN角色才能访问 - 其他路径允许所有人访问 - 通过表单登录进行身份验证 3. 配置用户信息 可以使用内存、数据库或LDAP等不同的方法来存储和管理用户信息。在内存中存储用户信息的示例代码如下: ```java @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER") .and() .withUser("admin").password("{noop}password").roles("ADMIN"); } ``` 上面的代码定义了两个用户: - 用户名为user,密码为password,角色为USER - 用户名为admin,密码为password,角色为ADMIN 4. 配置日志 可以使用日志记录Spring Security的操作和错误。例如,可以使用以下配置来记录Spring Security的操作: ```xml <configuration> <appender name="security" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="/var/log/security.log"/> <param name="MaxFileSize" value="10MB"/> <param name="MaxBackupIndex" value="10"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{ISO8601} %-5p [%t] %c{1} - %m%n"/> </layout> </appender> <logger name="org.springframework.security" additivity="false"> <level value="DEBUG"/> <appender-ref ref="security"/> </logger> </configuration> ``` 上面的配置Spring Security的日志记录到/var/log/security.log文件中。 以上是使用Spring Security的基本步骤,可以根据需要进行更多的配置和扩展。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值