0
点赞
收藏
分享

微信扫一扫

权限管理02-springboot整合springsecurity

向上的萝卜白菜 2022-02-05 阅读 43

1 加入pom.xml

<!--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.2.5</version>
        </dependency>
        <!--mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <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.1</version>
        </dependency>

2 配置application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot-demo07-security?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

spring.datasource.druid.initialSize=5
spring.datasource.druid.minIdle=5
spring.datasource.druid.maxActive=20
spring.datasource.druid.maxWait=60000
spring.datasource.druid.timeBetweenEvictionRunsMillis=60000
spring.datasource.druid.minEvictableIdleTimeMillis=300000
spring.datasource.druid.validationQuery=SELECT 1
spring.datasource.druid.testWhileIdle=true
spring.datasource.druid.testOnBorrow=true
spring.datasource.druid.testOnReturn=false
spring.datasource.druid.poolPreparedStatements=true
spring.datasource.druid.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.druid.filters=stat,wall
spring.datasource.druid.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=0


mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.auto-mapping-behavior=full
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:mapper/*Mapper.xml

server.port=9001

jwt.header=Authorization
jwt.expire=604800
jwt.secret=ji8n3439n439n43ld9ne9343fdfer49h

3 自定义用户认证的核心逻辑

1 创建UserDetailServiceImpl实现UserDetailsService

package com.grm.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.grm.entity.SysUser;
import com.grm.enums.CodeEnum;
import com.grm.exception.BusinessException;
import com.grm.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 自定义用户认证的核心逻辑
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    SysUserService sysUserService;

    /**
     * 需要构造出 org.springframework.security.core.userdetails.User 对象并返回
     *
     * @param username 用户名
     * @return {@link UserDetails}
     * @throws UsernameNotFoundException 用户名没有发现异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        SysUser sysUser = sysUserService.getOne(wrapper);
        if (sysUser == null) {
            throw new BusinessException(CodeEnum.USER_ACCOUNT_NOT_EXIST);
        }
        return new LoginUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
    }

    /**
     * 获取用户权限信息(角色、菜单权限)
     *
     * @param userId 用户id
     * @return {@link List}<{@link GrantedAuthority}>
     */
    public List<GrantedAuthority> getUserAuthority(Long userId) {
        // ROLE_admin,ROLE_normal,sys:user:list,....
        String authority = sysUserService.getUserAuthorityInfo(userId);
        //为用户分配权限,上面的配置类会根据权限来限制访问,产生不同结果。
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
    }
}

这里边,loadUserByUsername方法返回的是security包中的UserDetails对象

所以,我们要自定义LoginUser实现UserDetails返回给loadUserByUsername

package com.grm.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 最终交给Spring Security的是UserDetails
 * 用户的权限集, 默认需要添加ROLE_前缀
 * 用户的加密后的密码,不加密会使用{noop}前缀
 * 应用内唯一的用户名
 * 账户是否过期
 * 账户是否锁定
 * 凭证是否过期
 * 用户是否可用
 * <p>
 * 登录用户实现UserDetails接口
 */
public class LoginUser implements UserDetails {
    private Long userId;
    private String password;
    private final String username;
    // 已授予的权限
    private final Collection<? extends GrantedAuthority> authorities;
    // 账户是否过期
    private final boolean accountNonExpired;
    // 账户是否锁定
    private final boolean accountNonLocked;
    // 凭证是否过期
    private final boolean credentialsNonExpired;
    // 用户是否可用
    private final boolean enabled;

    public LoginUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(userId, username, password, true, true, true, true, authorities);
    }


    public LoginUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
                     boolean credentialsNonExpired, boolean accountNonLocked,
                     Collection<? extends GrantedAuthority> authorities) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = authorities;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

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

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

但是,用户的权限也得查询出来返回回去,具体体现在UserDetails中的   

所以我们要封装一个方法,根据userId查询authorities

 其中sysUserService.getUserAuthorityInfo(userId)具体实现如下

package com.grm.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.grm.entity.SysMenu;
import com.grm.entity.SysRole;
import com.grm.entity.SysUser;
import com.grm.mapper.SysMenuMapper;
import com.grm.mapper.SysRoleMapper;
import com.grm.mapper.SysUserMapper;
import com.grm.service.SysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 用户实现层
 *
 * @author gaorimao
 * @date 2022-02-04
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SysUserServiceImpl.class);

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private SysMenuMapper sysMenuMapper;

    @Override
    public String getUserAuthorityInfo(Long userId) {
        String authority;
        // 1.获取角色代码
        QueryWrapper<SysRole> sysRoleQueryWrapper = new QueryWrapper<>();
        String queryRoleIdsSql = "select role_id from sys_user_role where user_id = " + userId;
        sysRoleQueryWrapper.inSql("id", queryRoleIdsSql);
        List<SysRole> roles = sysRoleMapper.selectList(sysRoleQueryWrapper);
        String roleCodes = "";
        if (roles.size() > 0) {
            // 查询角色代码
            roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
            LOGGER.info("roleCodes==================={}",roleCodes);
        }
        authority = roleCodes.concat(",");
        LOGGER.info("authorityRole==================={}",authority);
        // 2.获取权限代码
        // 2.1 如果角色有admin,默认所有菜单权限
        String menuPerms = "";
        if (roles.size() > 0) {
            List<SysRole> adminRoles = roles.stream().filter(role -> role.getCode().equals("admin")).collect(Collectors.toList());
            if (adminRoles.size() > 0) {
                List<SysMenu> menus = sysMenuMapper.selectList(null);
                // 查询菜单权限
                menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
            }else{
                menuPerms = getPermsAuthorityByUserId(userId);
            }
            LOGGER.info("authorityMenuPerms==================={}",menuPerms);
        }
        authority = authority.concat(menuPerms);
        //  ROLE_admin,ROLE_normal,sys:user:list,sys:role:list...
        LOGGER.info("authorityAll==================={}",authority);
        return authority;
    }

    /**
     * 通过userId查询所有相关的menuIds
     *
     * @param userId 用户id
     * @return {@link String}
     */
    private String getPermsAuthorityByUserId(Long userId) {
        List<Long> menuIds = sysUserMapper.queryMenuIdsByUserId(userId);
        String menuPerms = "";
        if (menuIds.size() > 0) {
            List<SysMenu> menus = sysMenuMapper.selectBatchIds(menuIds);
            // 查询菜单权限
            menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
        }
        return menuPerms;
    }
}

其中sysUserMapper.queryMenuIdsByUserId(userId)方法需要自己写sql实现下

 代码如下

package com.grm.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.grm.entity.SysUser;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface SysUserMapper extends BaseMapper<SysUser> {

    @Select("SELECT DISTINCT srm.menu_id " +
            "FROM sys_user_role sur LEFT JOIN sys_role_menu srm " +
            "ON sur.role_id = srm.role_id " +
            "WHERE sur.user_id = #{userId} ")
    List<Long> queryMenuIdsByUserId(Long userId);
}

4 配置SecurityConfig

1 开启三个注解,实现两个方法,配置加密方式

package com.grm.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

/**
 * security配置认证和授权
 *
 * @author gaorimao
 * @since 2022-02-03
 */
@Configuration
//开启Spring Security的功能
@EnableWebSecurity
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String[] URL_WHITELIST = {
            "/login",
            "/logout",
    };

    /**
     * 配置认证方式等
     *
     * @param auth 身份验证
     * @throws Exception 异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式等
        super.configure(auth);
    }

    /**
     * security配置,包括登入登出、异常处理、会话管理等
     *
     * @param http http
     * @throws Exception 异常
     */
    protected void configure(HttpSecurity http) throws Exception {
    }

    /**
     * 加密方式
     *
     * @return {@link BCryptPasswordEncoder}
     */
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2 配置认证信息

    @Bean
    public UserDetailsService userDetailsService() {
        //获取用户账号密码及权限信息
        return new UserDetailServiceImpl();
    }

    /**
     * 配置认证方式等
     *
     * @param auth 身份验证
     * @throws Exception 异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式
        auth.userDetailsService(userDetailsService());
    }

 3 屏蔽Spring Security默认重定向登录页面以实现前后端分离功能

实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。

AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)

package com.grm.security;

import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.enums.CodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * 未登录,或者登录状态过期失效
 *
 * @author gaorimao
 * @date 2022-02-04
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
	private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		Result result = Result.failed(CodeEnum.USER_NOT_LOGIN);
		LOGGER.info("[JwtAuthenticationEntryPoint] result = {}", JSON.toJSONString(result));
		response.setContentType("text/json;charset=utf-8");
		response.getWriter().write(JSON.toJSONString(result));
	}
}

再在SecurityConfig加入认证失败处理的配置

(加入了csrf禁用、关闭session的代码)

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    /**
     * security配置,包括登入登出、异常处理、会话管理等
     *
     * @param http http
     * @throws Exception 异常
     */
    protected void configure(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()

    }

4 登录成功,登录失败,退出登录逻辑

(1) 登录成功

package com.grm.security.handler;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录成功处理程序
 *
 * @author gaorimao
 * @date 2022-02-04
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginSuccessHandler.class);

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展


        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        // 生成jwt,并放置到请求头中
        String jwt = jwtUtils.generateToken(authentication.getName());
        response.setHeader(jwtUtils.getHeader(), jwt);
        Result result = Result.success();
        LOGGER.info("[LoginSuccessHandler] result = {}", JSON.toJSONString(result));
        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

其中JwtUtils代码如下

package com.grm.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {
    private long expire;
    private String secret;
    private String header;

    // 生成jwt
    public String generateToken(String username) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)// 7天過期
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 解析jwt
    public Claims getClaimByToken(String jwt) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    // jwt是否过期
    public boolean isTokenExpired(Claims claims) {
        return claims.getExpiration().before(new Date());
    }
}

(2)登录失败逻辑

package com.grm.security.handler;

import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.enums.CodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * 登录失败处理程序
 *
 * @author gaorimao
 * @date 2022-02-04
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(LoginFailureHandler.class);

    private static final String BAD_CREDENTIALS = "Bad credentials";

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        Result result;
        if (BAD_CREDENTIALS.equals(exception.getMessage())) {
            result = Result.failed(CodeEnum.COMMON_FAIL);
        } else {
            result = Result.failed(exception.getMessage());
        }
        LOGGER.info("[LoginFailureHandler] result = {}", JSON.toJSONString(result));
        outputStream.write(JSON.toJSONString(result).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

(3)退出登录

package com.grm.security.handler;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.grm.common.Result;
import com.grm.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;

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

/**
 * 退出登录成功
 *
 * @author gaorimao
 * @date 2022-02-04
 */
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
	private static final Logger LOGGER = LoggerFactory.getLogger(JwtLogoutSuccessHandler.class);

	@Autowired
	private JwtUtils jwtUtils;

	@Override
	public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		if (authentication != null) {
			new SecurityContextLogoutHandler().logout(request, response, authentication);
		}
		response.setContentType("application/json;charset=UTF-8");
		ServletOutputStream outputStream = response.getOutputStream();
		response.setHeader(jwtUtils.getHeader(), "");
		Result result = Result.success();
		LOGGER.info("[JwtLogoutSuccessHandler] result = {}", JSON.toJSONString(result));
		outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
		outputStream.flush();
		outputStream.close();
	}
}

把登录成功、登录失败、退出登录加入到SecurityConfig配置中

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Autowired
    private JwtLogoutSuccessHandler logoutSuccessHandler;    

protected void configure(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);

        // 登录
        http.formLogin().
                permitAll().//允许所有用户
                successHandler(loginSuccessHandler).//登录成功处理逻辑
                failureHandler(loginFailureHandler);//登录失败处理逻辑
        //登出
        http.logout().
                permitAll().//允许所有用户
                logoutSuccessHandler(logoutSuccessHandler).//登出成功处理逻辑
                deleteCookies("JSESSIONID");//登出之后删除cookie

    }
举报

相关推荐

0 条评论