Springboot集成SpringSecurity

一、权限模型设计

t_user 用户表, 保存用户登录信息,和企业id

t_ent_info 企业表,通过ent_id关联多个用户,此时相当于用户组

t_ent_ent_role 企业角色表,一个企业可以关联多个角色

t_ent_role 角色表,角色基本属性

t_ent_permission 权限表,保存权限菜单, 菜单的各类属性

t_ent_role_permission 角色权限表,一个角色对应多个权限

二、springboot整合springsecurity

  • 前后端分离项目,后台无法通过url重定向,因此返回自定义Handler返回JSON数据给前端
  • 存在Session跨域问题,前后端分离情况下,后端springsecurity无法运行默认的SessionStrategy创建session。开发模式session由前端处理,正式环境通过nginx处理
  • @PreAuthorize注解hasRole("admin"),高版本默认加上前缀ROLE_, 是否有ROLE_admin角色

1、自定义Handler类,解决重定向问题

package com.owinfo.b2b.security;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

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

/**
 * 前后端分离、自定义处理器
 * @date 2019-05-23
 * @author pengjunjie
 */
@Log4j2
public class SecurityHandler {

	/**
	 * 拦截用返回JSON取代重定向
	 */
	public static class CustomLoginEntryPoint extends
			LoginUrlAuthenticationEntryPoint {

		public CustomLoginEntryPoint(String loginFormUrl) {
			super(loginFormUrl);
		}

		@Override
		public void commence(HttpServletRequest request, HttpServletResponse response,
		                     AuthenticationException authException) throws IOException {
			response.setContentType("application/json;charset=utf-8");
			ObjectMapper objectMapper = new ObjectMapper();
			PrintWriter out = response.getWriter();
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
			jsonObject.put("msg", "未登录!");
			out.write(objectMapper.writeValueAsString(jsonObject));
			out.flush();
			out.close();
		}
	}

	/**
	 * 登录成功处理, 将认证信息返给前端
	 */
	public static class CustomAuthenticationSuccessHandler extends
			SavedRequestAwareAuthenticationSuccessHandler {
		@Override
		public void onAuthenticationSuccess(HttpServletRequest request,
		                                    HttpServletResponse response,
		                                    Authentication authentication)
				throws IOException {
			response.setContentType("application/json;charset=UTF-8");
			ObjectMapper objectMapper = new ObjectMapper();

			JSONObject jsonObject = new JSONObject();
			jsonObject.put("status", HttpStatus.OK.value());
			jsonObject.put("msg", "登录成功!");
			jsonObject.put("userInfo", authentication.getPrincipal());
			PrintWriter out = response.getWriter();
			out.write(objectMapper.writeValueAsString(jsonObject));
			out.flush();
			out.close();
		}
	}

	/**
	 * 登录失败处理, 将异常信息返给前端
	 */
	public static class CustomAuthenticationFailureHandler extends
			SimpleUrlAuthenticationFailureHandler {
		@Override
		public void onAuthenticationFailure(HttpServletRequest request,
		                                    HttpServletResponse response,
		                                    AuthenticationException exception)
				throws IOException {
			String msg;
			if (exception instanceof BadCredentialsException) {
				msg = "密码错误,请重新输入";
			} else if (exception instanceof UsernameNotFoundException) {
				msg = "当前用户不存在";
			} else {
				msg = exception.getMessage();
			}

			response.setContentType("application/json;charset=UTF-8");
			ObjectMapper objectMapper = new ObjectMapper();
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
			jsonObject.put("msg", msg);

			PrintWriter out = response.getWriter();
			out.write(objectMapper.writeValueAsString(jsonObject));
			out.flush();
			out.close();
		}
	}

	/**
	 * 退出成功处理逻辑
	 */
	public static class CustomLogoutHandler extends
			SimpleUrlLogoutSuccessHandler {

		@Override
		public void onLogoutSuccess(HttpServletRequest request,
		                            HttpServletResponse response,
		                            Authentication authentication) throws IOException {
			response.setContentType("application/json;charset=UTF-8");
			ObjectMapper objectMapper = new ObjectMapper();
			JSONObject jsonObject = new JSONObject();
			jsonObject.put("status", HttpStatus.OK.value());
			jsonObject.put("msg", "退出登录成功!");

			PrintWriter out = response.getWriter();
			out.write(objectMapper.writeValueAsString(jsonObject));
			out.flush();
			out.close();
		}
	}
}

2、配置Handler和认证服务,封装USER对象

自定义UserDetails接口对象

package com.owinfo.b2b.security;

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

import java.util.List;

/**
 * 认证用户信息
 * @date 2019-05-21
 * @author pengjunjie
 */
public class EntUserInfo implements UserDetails {

	/**
	 *  用户UUID
	 */
	private String id;

	/**
	 * 用户名
	 */
	private String username;

	/**
	 * 密码
	 */
	private String password;

	/**
	 * 用户电话号码
	 */
	private String phone;

	/**
	 * 企业id
	 */
	private String entId;

	/**
	 * 企业海关编码
	 */
	private String entCode;


	/**
	 * 是否可用
	 */
	private Boolean enabled = true;

	/**
	 *用户所拥有的权限
	 */
	private List<? extends GrantedAuthority> authorities;

	/**
	 * 用户的账号是否过期,过期的账号无法通过授权验证. true 账号未过期
	 */
	private Boolean accountNonExpired = true;

	/**
	 * 用户的账户是否被锁定,被锁定的账户无法通过授权验证. true 账号未锁定
	 */
	private Boolean accountNonLocked = true;

	/**
	 * 用户的凭据(pasword) 是否过期,过期的凭据不能通过验证. true 没有过期,false 已过期
	 */
	private Boolean credentialsNonExpired = true;

	public String getId() {
		return id;
	}

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

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

	public String getPhone() {
		return phone;
	}

	public String getEntId() {
		return entId;
	}

	public String getEntCode() {
		return entCode;
	}

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

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

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

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

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

	public void setId(String id) {
		this.id = id;
	}

	public void setUsername(String username) {
		this.username = username;
	}

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

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public void setEntId(String entId) {
		this.entId = entId;
	}

	public void setEntCode(String entCode) {
		this.entCode = entCode;
	}

	public void setEnabled(Boolean enabled) {
		this.enabled = enabled;
	}

	public void setAuthorities(List<? extends GrantedAuthority> authorities) {
		this.authorities = authorities;
	}

	public void setAccountNonExpired(Boolean accountNonExpired) {
		this.accountNonExpired = accountNonExpired;
	}

	public void setAccountNonLocked(Boolean accountNonLocked) {
		this.accountNonLocked = accountNonLocked;
	}

	public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
		this.credentialsNonExpired = credentialsNonExpired;
	}

	@Override
	public String toString() {
		return "EntUserInfo{" +
				"id='" + id + '\'' +
				", username='" + username + '\'' +
				", password='" + password + '\'' +
				", phone='" + phone + '\'' +
				", entId='" + entId + '\'' +
				", entCode='" + entCode + '\'' +
				", enabled=" + enabled +
				", authorities=" + authorities +
				", accountNonExpired=" + accountNonExpired +
				", accountNonLocked=" + accountNonLocked +
				", credentialsNonExpired=" + credentialsNonExpired +
				'}';
	}
}

用户认证,在role前面加上ROLE_前缀

package com.owinfo.b2b.security;

import com.owinfo.b2b.service.dao.EntAccessDao;
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.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;

/**
 * 认证授权
 * @date 2019-05-21
 * @author pengjunjie
 */
@Service
public class EntUserDetail implements UserDetailsService {
	private static final String ROLE_PREFIX = "ROLE_";
	@Autowired
	private EntAccessDao accessDao;

	/**
	 * 将用户信息和角色信息放入SecurityContext中
	 * @param username
	 * @return
	 * @throws UsernameNotFoundException
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		EntUserInfo userInfo = accessDao.getUserByName(username);
		if (userInfo == null) {
			throw new UsernameNotFoundException("当前用户不存在");
		}

		String entId = userInfo.getEntId();
		List<String> roleCodeList = accessDao.getRoleListByEntId(entId);

		List<GrantedAuthority> authorityList;
		if (roleCodeList != null && roleCodeList.size() > 0) {
			authorityList = new ArrayList<>(roleCodeList.size() + 1);
			authorityList.add(new SimpleGrantedAuthority(ROLE_PREFIX + RoleConstant.USER));
			for (int i = 0; i < roleCodeList.size(); i++) {
				authorityList.add(new SimpleGrantedAuthority(ROLE_PREFIX + roleCodeList.get(i)));
			}
		} else {
			authorityList = new ArrayList<>(1);
			authorityList.add(new SimpleGrantedAuthority(ROLE_PREFIX + RoleConstant.USER));
		}

		userInfo.setAuthorities(authorityList);
		return userInfo;
	}
}

配置密码加密,handler,和用户认证服务

package com.owinfo.b2b.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsUtils;

/**
 * 登录认证配置
 * @date 2019-05-21
 * @author pengjunjie
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private EntUserDetail entUserDetail;

	private SecurityProperties properties;

	@Autowired
	public void setEntUserDetail(EntUserDetail entUserDetail) {
		this.entUserDetail = entUserDetail;
	}

	@Autowired
	public void setProperties(SecurityProperties properties) {
		this.properties = properties;
	}

	/**
	 * 访问地址配置
	 * 取消login、logout重定向操作、不再需要url
	 * @param http
	 * @throws Exception
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		/** 解决spring security跨域问题 */
		ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
				registry = http
				.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
				.and()
				.authorizeRequests()
				.requestMatchers(CorsUtils::isPreFlightRequest)
				.permitAll();

		String[] uriArray = properties.getPermitAll().split(",");
		for (int i = 0; i < uriArray.length; i++) {
			registry.antMatchers(uriArray[i]).permitAll();
		}
		registry.antMatchers("/**").authenticated()
				.and()
				.formLogin().loginProcessingUrl("/login")
				.successHandler(authenticationSuccessHandler())
				.failureHandler(authenticationFailureHandler())
				.and()
				.logout()
				.logoutSuccessHandler(simpleUrlLogoutHandler())
				.invalidateHttpSession(true)
				.and()
				.exceptionHandling().authenticationEntryPoint(loginUrlAuthenticationEntryPoint())
				.and()
				.cors()
				.and()
				.csrf()
				.disable();
	}


	/**
	 * 自定义认证服务
	 * @param auth
	 * @throws Exception
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) {
		auth.authenticationProvider(authenticationProvider());
	}

	/**
	 * 密码加密
	 */
	@Bean
	public BCryptPasswordEncoder passwordEncoder(){
		return new BCryptPasswordEncoder();
	}

	/**
	 *  抛出用户不存在异常
	 */
	@Bean
	public DaoAuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
		provider.setHideUserNotFoundExceptions(false);
		provider.setUserDetailsService(entUserDetail);
		provider.setPasswordEncoder(passwordEncoder());
		return provider;
	}

	@Bean
	public SecurityHandler.CustomLoginEntryPoint loginUrlAuthenticationEntryPoint() {
		return new SecurityHandler.CustomLoginEntryPoint("http://example.com");
	}

	@Bean
	public SecurityHandler.CustomAuthenticationSuccessHandler authenticationSuccessHandler() {
		return new SecurityHandler.CustomAuthenticationSuccessHandler();
	}

	@Bean
	public SecurityHandler.CustomAuthenticationFailureHandler authenticationFailureHandler() {
		return new SecurityHandler.CustomAuthenticationFailureHandler();
	}

	@Bean
	public SecurityHandler.CustomLogoutHandler simpleUrlLogoutHandler() {
		return new SecurityHandler.CustomLogoutHandler();
	}

}

提供获取用户和判断是否为管理员角色的统一方法

package com.owinfo.b2b.security;

import com.owinfo.b2b.service.dao.EntAccessDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 获取当前登录用户
 * @date 2019-05-22
 * @author pengjunjie
 */
@Component
public class UserContext {
	@Autowired
	private EntAccessDao entAccessDao;

	private static final String ROLE_ADMIN = "ROLE_admin";

	public EntUserInfo getCurrentUser() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication instanceof UsernamePasswordAuthenticationToken) {
			return (EntUserInfo) authentication.getPrincipal();
		} else {
			return null;
		}
	}

	/**
	 * 设置普通用户权限
	 * @param entId 企业id
	 * @return
	 */
	public void setUserRole(String entId) {
		String userRoleId = entAccessDao.getUserRoleId("user");
		Map map = new HashMap(2);
		map.put("entId", entId);
		map.put("userRoleId", userRoleId);
		entAccessDao.setUserRole(map);
	}


	/**
	 * 判断当前登录用户是否是管理员
	 * @return
	 */
	public boolean hasRoleAdmin() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication instanceof UsernamePasswordAuthenticationToken) {
			Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
			for (GrantedAuthority authority: authorities) {
				if (ROLE_ADMIN.equals(authority.getAuthority())) {
					return true;
				}
			}
		}
		return false;
	}
}

3、前后端分离无法创建session问题解决方案

自定义拦截器,对Response进行统一属性赋值

package com.owinfo.b2b.security;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录拦截器,处理session问题
 * @date 2019-05-23
 * @author pengjunjie
 */
public class LoginInterceptor implements HandlerInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request,
	                         HttpServletResponse response, Object object) {
		response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
		response.setHeader("Access-Control-Max-Age", "3600");
		response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
		response.setHeader("Access-Control-Allow-Credentials","true");
		return true;
	}
}

拦截地址

package com.owinfo.b2b.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * 前后端分离解决无法创建session问题
 * @date 2019-05-23
 * @author pengjunjie
 */
@Configuration
public class LoginConfig extends WebMvcConfigurerAdapter {
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/login");
       super.addInterceptors(registry);
   }
}

4、nginx解决跨域问题

使前端地址和后台访问地址为同一个域,前缀进行url重写

server {
        listen 8082;
        server_name 192.168.0.218;

        location / {
                     alias /home/owinfo/b2b/web/B2BWeb/dist/;
                     index  index.html index.htm;
                 }

        location /b2b {
                rewrite  ^.+b2b/?(.*)$ /$1 break;
                include  uwsgi_params;
                proxy_pass   http://192.168.0.218:8887;
       }
    }

三、PasswordEncoder业务数据 + salt盐加密方式

自定义PasswordEncoder,作用是只对密码进行equals比较。我们数据库的密码是加密后的密码,charSequence需要我们自己加密

JWTPasswordEncoder.java

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

/**
 * 密码不加密设置
 * @date 2019-06-24
 * @author pengjunjie
 */
public class JWTPasswordEncoder implements PasswordEncoder {

	@Override
	public String encode(CharSequence charSequence) {
		return charSequence.toString();
	}

	@Override
	public boolean matches(CharSequence charSequence, String encodePassword) {
		if (charSequence.toString().equals(encodePassword)) {
			return true;
		}
		return false;
	}

}

 自定义Provider更改密码验证方式,这里对密码进行加密,采用自定义的加密方式

EntDaoProvider.java

/**
 * 自定义密码加密方式
 * @date 2019-06-24
 * @author pengjunjie
 */
public class EntDaoProvider extends DaoAuthenticationProvider {

	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {

		Object credentials = authentication.getCredentials();
		if (credentials == null) {
			throw new BadCredentialsException("请输入密码");
		}

		// 老版本 salt设置null取"", 新版本自动生成salt,该如何修改salt呢? 可以重新对SHA-256加密方式进行定义
		MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("SHA-256");
		String encodePassword = encoder.encode(((String)credentials) + ((EntUserInfo)userDetails).getId());

		UsernamePasswordAuthenticationToken authenticationToken =
				new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), encodePassword);
		super.additionalAuthenticationChecks(userDetails, authenticationToken);
	}
}

 加密方式重写

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

final class Digester {
    private final String algorithm;
    private int iterations;

    public Digester(String algorithm, int iterations) {
        createDigest(algorithm);
        this.algorithm = algorithm;
        this.setIterations(iterations);
    }

    public byte[] digest(byte[] value) {
        MessageDigest messageDigest = createDigest(this.algorithm);

        for(int i = 0; i < this.iterations; ++i) {
            value = messageDigest.digest(value);
        }

        return value;
    }

    final void setIterations(int iterations) {
        if (iterations <= 0) {
            throw new IllegalArgumentException("Iterations value must be greater than zero");
        } else {
            this.iterations = iterations;
        }
    }

    private static MessageDigest createDigest(String algorithm) {
        try {
            return MessageDigest.getInstance(algorithm);
        } catch (NoSuchAlgorithmException var2) {
            throw new IllegalStateException("No such hashing algorithm", var2);
        }
    }
}

import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Base64;


/** 主要是可以通过业务数据,重写salt等等、这里salt设置为空串 */ @Deprecated public class MessageDigestPasswordEncoder implements PasswordEncoder { private boolean encodeHashAsBase64 = false; private Digester digester; public MessageDigestPasswordEncoder(String algorithm) { this.digester = new Digester(algorithm, 1); } public String encode(CharSequence rawPassword) { return this.digest("", rawPassword); } @Override public boolean matches(CharSequence charSequence, String s) { return false; } private String digest(String salt, CharSequence rawPassword) { String saltedPassword = rawPassword + salt; byte[] digest = this.digester.digest(Utf8.encode(saltedPassword)); String encoded = this.encode(digest); return salt + encoded; } private String encode(byte[] digest) { return this.encodeHashAsBase64 ? Utf8.decode(Base64.getEncoder().encode(digest)) : new String(Hex.encode(digest)); } }

 

posted @ 2019-05-31 16:30  陌生。  阅读(429)  评论(0)    收藏  举报