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));
}
}

浙公网安备 33010602011771号