springboot - 2.7.3版本 - (九)整合security

一,学习思路

1)引入依赖包,添加基础配置,使用默认的登陆页面以及登陆接口,了解认证授权的流程

2)添加动态权限控制,通过用户-角色-权限绑定,从数据库获取权限数据

3)添加自定义token,取消默认的session认证

4)自定义登陆页面,自定义登陆成功,失败,以及认证失败后的处理逻辑

参考博客:

SpringSecurity之授权_Littewood的博客-CSDN博客_springsecurity授权

springsecurity自定义角色权限授权_卧龙山上的大猴子的博客-CSDN博客_springsecurity角色权限控制

springBoot集成spring-security 自定义token实现_lzq199528的博客-CSDN博客_springsecurity自定义token

二,数据准备

1,用户-角色-权限 关系表

数据表表字段表数据说明
auth_user用户表
auth_role角色表
auth_permit权限表
auth_user_role用户角色关系表
auth_role_permit角色权限关系表

 2,默认分配三个用户,分别对应三个角色,不同角色拥有各自的权限。

测试用户分配角色分配权限
张三admin/admin/**,/user/**,/guest/**
李四user/user/**,/guest/**
王五guest/guest/**

 3,sql语句

DROP TABLE IF EXISTS auth_user;
CREATE TABLE auth_user(
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    created_time timestamp DEFAULT now() COMMENT '创建时间' ,
    updated_time timestamp COMMENT '修改时间' ,
    user_name VARCHAR(255) COMMENT '用户名' ,
    password VARCHAR(255) COMMENT '密码' ,
    remark VARCHAR(255) COMMENT '备注' ,
    PRIMARY KEY (id)
)  COMMENT = '用户信息';

INSERT INTO `auth_user` (user_name, password) VALUES('张三','123456');
INSERT INTO `auth_user` (user_name, password) VALUES('李四','123456');
INSERT INTO `auth_user` (user_name, password) VALUES('王五','123456');


DROP TABLE IF EXISTS auth_role;
CREATE TABLE auth_role(
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    created_time DATETIME DEFAULT now() COMMENT '创建时间' ,
    updated_time timestamp COMMENT '修改时间' ,
    name VARCHAR(255) COMMENT '角色名称' ,
    mark_name VARCHAR(255) COMMENT '标记名称' ,
    remark VARCHAR(255) COMMENT '备注' ,
    PRIMARY KEY (id)
)  COMMENT = '角色';

INSERT INTO `auth_role` (name, mark_name) VALUES('管理员','admin');
INSERT INTO `auth_role` (name, mark_name) VALUES('系统用户','user');
INSERT INTO `auth_role` (name, mark_name) VALUES('普通用户','guest');


DROP TABLE IF EXISTS auth_permit;
CREATE TABLE auth_permit(
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    created_time timestamp DEFAULT now() COMMENT '创建时间' ,
    updated_time timestamp COMMENT '修改时间' ,
    pid bigint(20) default null COMMENT '父菜单ID' ,
    name VARCHAR(255) COMMENT '权限名称' ,
    url VARCHAR(255) COMMENT '授权路径' ,
    remark VARCHAR(255)    COMMENT '备注' ,
    PRIMARY KEY (id)
)  COMMENT = '权限';

INSERT INTO `auth_permit` (name, url) VALUES('管理员权限','/admin/**');
INSERT INTO `auth_permit` (name, url) VALUES('系统用户权限','/user/**');
INSERT INTO `auth_permit` (name, url) VALUES('普通用户权限','/guest/**');


DROP TABLE IF EXISTS auth_user_role;
CREATE TABLE auth_user_role(
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    created_time timestamp DEFAULT now() COMMENT '创建时间' ,
    updated_time timestamp COMMENT '修改时间' ,
    user_id bigint(20) COMMENT '用户id' ,
    role_id bigint(20) COMMENT '角色id' ,
    PRIMARY KEY (id)
)  COMMENT = '用户角色表';

INSERT INTO `auth_user_role` (user_id, role_id) VALUES(1, 1);
INSERT INTO `auth_user_role` (user_id, role_id) VALUES(2, 2);
INSERT INTO `auth_user_role` (user_id, role_id) VALUES(3, 3);



DROP TABLE IF EXISTS auth_role_permit;
CREATE TABLE auth_role_permit(
    id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    created_time timestamp DEFAULT now() COMMENT '创建时间' ,
    updated_time timestamp COMMENT '修改时间' ,
    role_id bigint(20) COMMENT '角色ID' ,
    permit_id bigint(20) COMMENT '权限ID' ,
    PRIMARY KEY (id)
)  COMMENT = '角色权限表';

INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(1, 1);
INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(1, 2);
INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(1, 3);
INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(2, 2);
INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(2, 3);
INSERT INTO `auth_role_permit` (role_id, permit_id) VALUES(3, 3);

 4,代码中需要用到的查询语句

方法名说明语句
getByName根据用户名查询用户信息
select * from auth_user where user_name = ''

getByUserId根据用户ID查询用户所有角色
select r.* from auth_role r, auth_user_role ur where r.id = ur.role_id and ur.user_id = ''

getAllAndRole查询所有权限以及分配的角色
select p.*, r.id as roleId, r.name as roleName, r.mark_name as roleMarkName 
        from auth_permit p 
        left join auth_role_permit rp on p.id = rp.permit_id 
        left join auth_role r on rp.role_id = r.id

 三,在项目中使用

1,pom.xml引入依赖包

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

,2,security 开启配置

最新版配置不需要再继承WebSecurityConfigurerAdapter这个类做配置,取而代之的是配置相应的过滤器链来进行相关配置, 配置方式以及属性保持不变.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Resource
	MyUserDetailsService userDetailsService;
	
	/**
	 * 配置过滤器链
	 * @param http
	 * @return
	 * @throws Exception
	 */
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		// 启用Security登录,使用默认登陆页面
		http.formLogin();
		
		// 自定义从数据库查询用户信息
        http.userDetailsService(userDetailsService);
        
        // 请求访问策略
        http.authorizeRequests()
        .antMatchers("/public/**").permitAll() //放行资源,自定义添加
        .anyRequest().authenticated(); //除以上放行资源外,其他全部拦截
		return http.build();
	}

	
	/**
	 * 配置加密方法 - 否则会报错There is no PasswordEncoder mapped for the id “null”
	 * @return
	 */
	@Bean
	public BCryptPasswordEncoder encoding(){
		return new BCryptPasswordEncoder();
	}
	
}

,3,自定义的UserDetailsService,从数据库查询用户信息和角色,交给security做认证和授权

@Component
public class MyUserDetailsService implements UserDetailsService {

	@Resource
	private AuthUserService authUserService;
	
	@Resource
	private AuthRoleService authRoleService;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		//获取用户信息
		AuthUser authUser = authUserService.getByName(username);
		
		if (null == authUser) {
	        throw new UsernameNotFoundException("用户名不存在");
	    }
		//初始密码是手动添加到数据库的,没有加密,后面认证时会自动将用户提交的登陆密码加密后再与数据库密码校验,所以这里需要将明文密码进行加密
		authUser.setPassWord(new BCryptPasswordEncoder().encode(authUser.getPassword()));
		//获取用户角色
		authUser.setRoleList(authRoleService.getByUserId(authUser.getId()));
		
		return authUser;
	}

}

,4,用户实体类AuthUser 根据需要实现UserDetails接口

- 用于用户状态认证的字段表格里面没有,全部设置为true,否则登陆时会提示user is disabled

- 角色授权这里取字段mark_name,赋值用英文,另一字段name用于前端中文显示

@TableName("auth_user")
public class AuthUser extends BaseModel implements UserDetails{
	
	@TableField("user_name")
	private String userName;
	
	@TableField("password")
	private String passWord;
	
	@TableField(exist = false)
	private List<AuthRole> roleList;

	/**
	 * 角色授权 - 取角色名称集合
	 */
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return roleList.stream().map(r -> new SimpleGrantedAuthority(r.getMarkName())).collect(Collectors.toList());
	}

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

	@Override
	public String getUsername() {
		return this.userName;
	}

	/**
     * 帐户是否未过期,没有这个字段直接设置true
     */
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	/**
	 * 账号是否未锁定,没有这个字段直接设置true
	 */
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	/**
     * 密码是否未过期,没有这个字段直接设置true
     */
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	/**
	 * 账号是否启用
	 */
	@Override
	public boolean isEnabled() {
		return true;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassWord() {
		return passWord;
	}

	public void setPassWord(String passWord) {
		this.passWord = passWord;
	}

	public List<AuthRole> getRoleList() {
		return roleList;
	}

	public void setRoleList(List<AuthRole> roleList) {
		this.roleList = roleList;
	}
	

}

5,添加测试方法:

@RestController
public class SecurityController {
	
	private final static Logger LOGGER = LoggerFactory.getLogger(SecurityController.class);
	
	@GetMapping("/demo/hello")
	public JsonResult hello(){
		LOGGER.info("用户已登陆,可以访问");
		return JsonResultBuilder.ok("hello world!");
	}
	
	@GetMapping("/admin/hello")
    public JsonResult admin(){
		LOGGER.info("用户为admin角色,可以访问");
		return JsonResultBuilder.ok("hello admin");
    }
	
    @GetMapping("/user/hello")
    public JsonResult user(){
    	LOGGER.info("用户为admin,user角色,可以访问");
    	return JsonResultBuilder.ok("hello user");
    }

    @GetMapping("/guest/hello")
    public JsonResult guest(){
    	LOGGER.info("用户为admin,user,guest角色,可以访问");
    	return JsonResultBuilder.ok("hello guest");
    }


}

1)用户登录拦截测试

- 启动项目测试访问 /demo/hello会被拦截,自动跳转到/login页面要求登陆,这里并没有将对角色权限进行控制,所以任何一个账号登陆都可以访问。

 2)增加角色权限控制,启动测试,只有用户张三才可以成功访问

.antMatchers("/admin/**").hasRole("admin") //该路径只有角色admin用户才能访问

3)实现动态权限管理

实际项目中角色,权限都是可以变更的,如果向上面那样在配置中硬编码会出现很多问题,需要我们自定义实现角色权限的动态管理。

- 自定义设置权限的元数据

/**
 * 自定义接口 - 设置权限元数据
 * 找出当前url绑定的所有角色,后面再将用户角色与之比对,存在则可以访问,否则无权访问
 * @author Admin
 *
 */
@Component
public class MySecurityMetaSource implements FilterInvocationSecurityMetadataSource{
	private final static Logger LOGGER = LoggerFactory.getLogger(MySecurityMetaSource.class);
	
	@Resource
	private AuthPermitService authPermitService;
	
	AntPathMatcher antPathMatcher = new AntPathMatcher();
	
	@Override
	public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
		LOGGER.info("MySecurityMetaSource - 自定义权限元数据开启");
		
		//1.当前请求对象
        String requestURI = ((FilterInvocation)object).getRequest().getRequestURI();
        LOGGER.info("MySecurityMetaSource - 当前访问路径:{}", requestURI);
        // 默认登陆接口放行
 		if (requestURI.endsWith("login")) {
 			LOGGER.info("MySecurityMetaSource - 白名单放行,不校验请求权限");
 			return null;
 		}
        
        //2.查询所有权限和角色
        List<AuthPermit> permitList = authPermitService.getAllAndRole();
        
        for (AuthPermit permit : permitList) {
            if (antPathMatcher.match(permit.getUrl(), requestURI)){
                String[] roles = permit.getRoleList().stream().map(r -> r.getMarkName()).toArray(String[] :: new);
                return SecurityConfig.createList(roles);
            }
        }
        
        /*
         * 如果返回null,就不会进行下一步的角色对比,所有用户都可以访问
         * 如果需要对未分配角色的路由进行拦截,就像下面写法一样默认分配一个ROLE_UNKNOWN(名称自定义,不能与现有角色重名)角色,
         * 后面对用户角色进行匹配时就会提示无权访问
         */
//		return null;
        return SecurityConfig.createList("ROLE_UNKNOWN");
	}

	@Override
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		return null;
	}

	@Override
	public boolean supports(Class<?> clazz) {
		return false;
	}

}

- 自定义角色权限判断

/**
 * 自定义角色权限的判断
 * @author Admin
 *
 */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

	private final static Logger LOGGER = LoggerFactory.getLogger(MyAccessDecisionManager.class);
	
	@Override
	public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException {
		LOGGER.info("MyAccessDecisionManager - 自定义权限判断开启");
		
		// 1,取出当前路由分配的所有角色
		List<String> permitRoles = configAttributes.stream().map(e -> e.getAttribute()).collect(Collectors.toList());
        LOGGER.info("MyAccessDecisionManager - 当前访问路径授权角色:{}", permitRoles);

		
		// 2,判断当前路由是否未分配角色
		if (permitRoles.contains("ROLE_UNKNOWN")) {
			throw new AccessDeniedException("权限未开放,请联系管理员!");
		};
		
		// 3,用户角色和路由角色比对
		for (GrantedAuthority grantedAuthority : authentication.getAuthorities()){
			if (permitRoles.contains(grantedAuthority.getAuthority())) {
				return;
			}
		}
		
		throw new AccessDeniedException("权限不足,请联系管理员!");
	}

	@Override
	public boolean supports(ConfigAttribute attribute) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean supports(Class<?> clazz) {
		// TODO Auto-generated method stub
		return false;
	}

}

- 将上面自定义的组件加入到security配置中

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Resource
	MyUserDetailsService userDetailsService;
	@Resource
	MySecurityMetaSource securityMetaSource;
	@Resource
	MyAccessDecisionManager accessDecisionManager;
	
	/**
	 * 配置过滤器链
	 * @param http
	 * @return
	 * @throws Exception
	 */
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		// 启用Security登录,使用默认登陆页面
		http.formLogin();
		
		// 自定义从数据库查询用户信息
        http.userDetailsService(userDetailsService);
        
        // 请求访问策略
        http.authorizeRequests()
        .antMatchers("/public/**").permitAll() //放行资源,自定义添加
//        .antMatchers("/admin/**").hasRole("admin") //该路径只有角色admin用户才能访问
        .anyRequest().authenticated() //除以上放行资源外,其他全部拦截
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {

			@Override
			public <O extends FilterSecurityInterceptor> O postProcess(O object) {
				//自定义需要设置的角色权限
				object.setSecurityMetadataSource(securityMetaSource);
				object.setAccessDecisionManager(accessDecisionManager);
                //是否拒绝公共资源访问
                object.setRejectPublicInvocations(false);
				return object;
			}
        	
        });
		return http.build();
	}
	
	/**
	 * 配置加密方法 - 否则会报错There is no PasswordEncoder mapped for the id “null”
	 * @return
	 */
	@Bean
	public BCryptPasswordEncoder encoding(){
		return new BCryptPasswordEncoder();
	}
	
}

- 启动测试

a. 访问路径/demo/hello,未分配角色权限,所有用户都无权访问

b. 访问路径/admin/hello,只有用户张三绑定了admin角色才能访问

c. 访问路径/user/hello,用户张三和李四可以访问

d. 访问路径/guest/hello,用户张三,李四,王五都可以访问

注意数据库配置权限路由的通配符去匹配:

四,添加自定义token认证

实现思路:

1,在security提供的过滤器UsernamePasswordAuthenticationFilter之前加入我们自定义的TokenFilter,先判断token是否存在,如果存在则取出用户数据Authentication对象放到SecurityContext上下文中

2,自定义登陆成功处理器,生成token,保存用户信息Authentication,并返回token给前端

3,修改sercurity配置,关闭session认证方式,并加入上面自定义的类

=====================================================================

对于token的存储方式:

1,数据库:这里不做演示

2,redis:推荐方式,但是测试过程中遇到一个问题,就是从redis中取出json数据反序列化为Authentication对象时报错,无法正常转换,程序中断,后面找到解决方案再更新。

3,内存:此处演示用此方法,添加操作类TokenAndAuthentication 

public class TokenAndAuthentication {

    private static ConcurrentHashMap<String, Authentication> map = new ConcurrentHashMap();

    public static Authentication getAuthentication(String token) {
        return map.get(token);
    }

    public static void setTokenAndAuthentication(String token, Authentication authentication) {
        map.put(token, authentication);
    }

    public static void delete(String token) {
        map.remove(token);
    }
}

=========================================================================

- 自定义LoginSuccessHandler,登陆成功返回token

/**
 * 登陆成功处理器 - 返回前端token
 * @author Admin
 *
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

	private final static Logger LOGGER = LoggerFactory.getLogger(LoginSuccessHandler.class);
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		LOGGER.info("LoginSuccessHandler - 登陆成功,生成Token,绑定用户信息保存到Redis并返回!");
		
        String token = UUIDGenerator.generate();
        TokenAndAuthentication.setTokenAndAuthentication(token, authentication);
        JsonResult jsonResult = JsonResultBuilder.ok(token);
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out;
        out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(jsonResult));
        out.flush();
        out.close();
	}

}

- 自定义token认证过滤器AuthorizationTokenFilter

@Component
public class AuthorizationTokenFilter extends OncePerRequestFilter{

	private final static Logger LOGGER = LoggerFactory.getLogger(AuthorizationTokenFilter.class);
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		LOGGER.info("AuthorizationTokenFilter - 自定义Token认证过滤器开启!");
		
		// 默认登陆接口放行
		if (request.getRequestURI().endsWith("login")) {
			LOGGER.info("AuthorizationTokenFilter - 白名单放行,不校验token");
			filterChain.doFilter(request, response);
			return;
		} 
		
		// 获取token
		String token = request.getHeader("Authorization");
		if (!StringUtils.hasLength(token)) {
			// 返回前端提示需要携带Token
			returnMsg(response, JsonResultBuilder.error(ReturnCode.TOKEN_FORBIDDEN));
			return;
		} 

		// 存在token则从内存中取出用户信息
		Authentication authentication = TokenAndAuthentication.getAuthentication(token);
		if (authentication != null) {
			// 添加至上下文中
			SecurityContextHolder.getContext().setAuthentication(authentication);
			filterChain.doFilter(request, response);
		} else {
			// 返回前端提示需要重新登陆
			returnMsg(response, JsonResultBuilder.error(ReturnCode.TOKEN_EXPIRE));
		}
		
	}

	/**
	 * Token认证失败返回前端提示
	 * @param response
	 * @param error
	 * @throws IOException
	 */
	private void returnMsg(HttpServletResponse response, JsonResult error) throws IOException {
		response.setContentType("application/json;charset=utf-8");
        PrintWriter out;
        out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(error));
        out.flush();
        out.close();
		
	}

}

- 更新security配置类如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Resource
	MyUserDetailsService userDetailsService;
	@Resource
	MySecurityMetaSource securityMetaSource;
	@Resource
	MyAccessDecisionManager accessDecisionManager;
	
	@Resource
	LoginSuccessHandler loginSuccessHandler;
	
	@Resource
	AuthorizationTokenFilter tokenFilter;
	
	/**
	 * 配置过滤器链
	 * @param http
	 * @return
	 * @throws Exception
	 */
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		// 启用Security登录,使用默认登陆页面
		http.formLogin()
	        .loginProcessingUrl("/login").permitAll() // 定义登录接口 默认 login
	        .successHandler(loginSuccessHandler); // 登录成功处理
		
		// 自定义从数据库查询用户信息
        http.userDetailsService(userDetailsService);
        
        // 请求访问策略
        http.authorizeRequests()
	        .antMatchers("/public/**").permitAll() //放行资源,自定义添加
//	        .antMatchers("/admin/**").hasRole("admin") //该路径只有角色admin用户才能访问
	        .anyRequest().authenticated() //除以上放行资源外,其他全部拦截
	        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {

				@Override
				public <O extends FilterSecurityInterceptor> O postProcess(O object) {
					//自定义需要设置的角色权限
					object.setSecurityMetadataSource(securityMetaSource);
					object.setAccessDecisionManager(accessDecisionManager);
	                //是否拒绝公共资源访问
	                object.setRejectPublicInvocations(false);
					return object;
				}
	        	
	        });
        
        // 添加过滤器,在token访问时 将权限信息加入上下文中
        http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.csrf().disable()	// 基于token,不需要csrf防御机制
        	.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);	//基于token,不创建session
        
		return http.build();
	}
	
	/**
	 * 配置加密方法 - 否则会报错There is no PasswordEncoder mapped for the id “null”
	 * @return
	 */
	@Bean
	public BCryptPasswordEncoder encoding(){
		return new BCryptPasswordEncoder();
	}
	
}

启动测试:

说明访问接口返回结果
登陆接口放行,走security提供的认证接口,未登录用户跳转到默认提供的登陆页面/login
获取token后使用postman测试访问,携带token/user/hello
访问无token提示/user/hello

五,源代码下载:https://download.csdn.net/download/MyNoteBlog/86749700

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值