前言:最近做项目的时候,需要角色和权限的认证,前后端分离,查阅一番资料后,网上的大多都不符合项目的使用标准,于是自己写个博客跟大家分享一番,有什么欠缺的地方留言讨论
文章目录:
- 首先需要了解Spring Security的过滤链和认证流程
- 其次了解流程之后需要根据项目的需求做一些认证的变动(不过大体是一样的)
- 建立数据库脚本,引用security的配置,开始愉快的写代码了
代码放Git上了:https://github.com/lulu0008/spring-security.git
一、首先介绍Spring Security的过滤链
下边列出Spring Security过滤链默认的执行顺序
参考这位博主写的比较全面:https://blog.csdn.net/andy_zhang2007/article/details/84726992
- WebAsyncManagerIntegrationFilter 【为请求处理过程中可能发生的异步调用准备安全上下文获取途径】
- SecurityContextPersistenceFilter
- HeaderWriterFilter 【请求的处理过程中为响应对象增加一些头部信息】
- LogoutFilter 【退出登录】
- UsernamePasswordAuthenticationFilter 【默认是使用form表单登录的(可以改为其他登录方式如:json登录)】
- JwtAuthorizationTokenFilter【这个过滤器是验证token在项目中使用OncePerRequestFilter(保证请求只有一次经过过滤器)】
- RequestCacheAwareFilter 【提取请求缓存中缓存的请求】
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
过滤链的流程图如下
参考这位博主的:https://blog.csdn.net/zhong_csdn/article/details/79447185
在下边图片步骤主要使用步骤5和6【主要使用的是这两个过滤器实现权限的登录】
参考这位博主的图片:https://blog.csdn.net/qq_1017097573/article/details/85873125
spring security的配置文件贴出来
package com.demo.security;
import com.demo.security.filter.JwtAuthenticationProvider;
import com.demo.security.filter.JwtAuthenticationTokenFilter;
import com.demo.security.filter.MyUsernamePasswordAuthenticationFilter;
import com.demo.security.handler.*;
import com.demo.security.service.JwtUserDetailsService;
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.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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsUtils;
import javax.annotation.Resource;
/**
* 权限配置中心
*/
@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)//是否支持web
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;//未登陆时返回 JSON 格式的数据给前端(否则为 html)
@Resource
private MySuccessHandler mySuccessHandler;//自定义的登录成功处理器
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler; //自定义的登录失败处理器
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler; //依赖注入自定义的注销成功的处理器
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;//注册没有权限的处理器
@Resource
private JwtUserDetailsService jwtUserDetailsService; //自定义user
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // 拦截token JWT 拦截器
@Resource
private JwtAuthenticationProvider jwtAuthenticationProvider; // 自定义登录
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里可启用我们自己的登陆验证逻辑,用户密码加密 放到jwtAuthenticationProvider中
//auth.userDetailsService(jwtUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
auth.authenticationProvider(jwtAuthenticationProvider);
}
/**
* 配置spring security的控制逻辑
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
String[] arrUrl = JwtAuthenticationTokenFilter.arrUrl;
// 新加入(cors) CSRF 取消跨站请求伪造防护
http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用 JWT,关闭token
//用户未登录
http.httpBasic().authenticationEntryPoint(myAuthenticationEntryPoint);
http.authorizeRequests()
/** 设置任何用户可以访问的路径 **/
.antMatchers(arrUrl).permitAll()
/** 解决跨域 **/
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
/** 任何尚未匹配的URL都只需要对用户进行身份验证 每个请求的url必须通过这个规则 RBAC 动态 url 认证 **/
.anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")
/**表单登录开始配置 表单登录使用的配置 不使用暂时注释 **/
// .and()
// .formLogin() //开启登录, 定义当需要用户登录时候,转到的登录页面
// .loginProcessingUrl("/user/login")//loginProcessingUrl用于指定前后端分离的时候调用后台登录接口的名称
// .successHandler(mySuccessHandler) // 登录成功
// .failureHandler(myAuthenticationFailureHandler) // 登录失败
/**表单登录结束配置 */
.and()
/** loginProcessingUrl用于指定前后端分离的时候调用后台注销接口的名称 如果启用了CSRF保护(默认),那么请求也必须是POST **/
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(myLogoutSuccessHandler)
.permitAll();
// 无权访问 JSON 格式的数据
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
//在执行MyUsernamePasswordAuthenticationFilter之前执行jwtAuthenticationTokenFilter
http.addFilterBefore(jwtAuthenticationTokenFilter, MyUsernamePasswordAuthenticationFilter.class);
//用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
http.addFilterAt(customAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
http.headers().cacheControl();
}
/**
* JSON登陆(注册登录的bean)
*/
@Bean
MyUsernamePasswordAuthenticationFilter customAuthenticationFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
filter.setAuthenticationSuccessHandler(mySuccessHandler);
filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
filter.setFilterProcessesUrl("/user/login");
//这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}
接下来就是上数据库的脚本:表设计比较简单可以后期根据自己的设计添加业务需求。
【表名统一以sys开头,角色以ROLE_(spring中建议)开头】
sys_user、sys_resource、sys_role三个主表用户、菜单、角色表。
sys_resource_role、sys_user_role(这个表可以不要,放到user表中也是可以的,不过违反的表设计的三大原则,个人感觉还是分开的好,毕竟根据业务的需求变动也好更改)两个关系表。
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_resource
-- ----------------------------
DROP TABLE IF EXISTS `sys_resource`;
CREATE TABLE `sys_resource` (
`id` int(11) NOT NULL,
`res_name` varchar(255) NOT NULL,
`url` varchar(255) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`remark` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`sort` int(11) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_resource_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_resource_role`;
CREATE TABLE `sys_resource_role` (
`id` int(11) NOT NULL,
`res_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(50) NOT NULL,
`password` varchar(50) DEFAULT NULL,
`type` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', 'admin', '1');
INSERT INTO `sys_user` VALUES ('2', 'user', '123456', '2');
-- ----------------------------
-- Records of sys_resource
-- ----------------------------
INSERT INTO `sys_resource` VALUES ('1', '首页', '/index', null, null);
INSERT INTO `sys_resource` VALUES ('2', '账号管理', '/user', null, null);
INSERT INTO `sys_resource` VALUES ('3', '系统管理', '/system', null, null);
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN', '1', 'ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER', '2', 'USER');
-- ----------------------------
-- Records of sys_resource_role
-- admin权限菜单
INSERT INTO `sys_resource_role` VALUES ('1', '1', '1');
INSERT INTO `sys_resource_role` VALUES ('2', '2', '1');
INSERT INTO `sys_resource_role` VALUES ('3', '3', '1');
-- user 权限菜单
INSERT INTO `sys_resource_role` VALUES ('5', '1', '2');
INSERT INTO `sys_resource_role` VALUES ('6', '2', '2');
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2', '2');
表设计好之后就开始新建立springboot项目了来配合使用security使用:
一、建立用户安全模型(JwtUser类)
package com.security.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
public class JwtUser implements UserDetails, Serializable {
private String username;
private String password;
//存放用户的角色信息
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(){}
public JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public String toString() {
return "JwtUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
@Override
public String getUsername() {
return username;
}
//账号是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
账号是否锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
///账号凭证是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
}
二、 token生成的工具类(JwtTokenUtil)
package com.security.utils;
import com.security.model.JwtUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenUtil implements Serializable {
private static String secret = "secret";
private static Long timeout = 60*60*2*1000L;//两小时
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + timeout);
return Jwts.builder()
.setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
三、处理器(handler)。用户登录成功、失败、没有权限、用户未登录、退出登录
1. 登录成功。用户通过security的验证鉴权会交给AuthenticationSuccessHandler来处理。在这里可以做一些关于用户的统计信息,再者把token和用户的信息一起放入redis中,过期时间和生成token的时间一致即可。
package com.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.JwtUser;
import com.security.utils.JwtTokenUtil;
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.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 登录成功
*/
@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(MySuccessHandler.class);
/**Json转化工具*/
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
JwtUser userDetails = (JwtUser)authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成token
String token = JwtTokenUtil.generateToken(userDetails);
Map<String,String> map = new LinkedHashMap<>();
map.put("code", String.valueOf(HttpServletResponse.SC_OK));
map.put("msg", "登录成功");
map.put("token", token);
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}
}
2. 登录失败。用户都没有通过security的验证会交给 AuthenticationFailureHandler来处理。
package com.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 登录失败
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String,String> map = new LinkedHashMap<>();
map.put("code", "10001");
map.put("msg", exception.getMessage());
map.put("msg_", "登录验证失败");
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}
}
3. 用户没有权限。用户都没有通过security的验证会交给 AccessdenieHandler来处理。
package com.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 没有权限处理的类
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Map<String,String> map = new LinkedHashMap<>();
map.put("code", String.valueOf(HttpServletResponse.SC_FORBIDDEN));
map.put("msg", e.getMessage());
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
try {
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}catch (IOException o){
o.printStackTrace();
if(writer != null){
writer.flush();
writer.close();
}
}
}
}
4. 用户未登录。用户都没有通过security的验证会交给 AuthenticationEntryPoint来处理。
package com.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 用户未登录时返回给前端的数据
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
Map<String,String> map = new LinkedHashMap<>();
map.put("code", String.valueOf(HttpServletResponse.SC_CREATED));
map.put("msg", e.getMessage());
map.put("msg1", "用户未登录");
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
try {
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}catch (IOException o){
o.printStackTrace();
if(writer != null){
writer.flush();
writer.close();
}
}
}
}
5.退出登录。用户都没有通过security的验证会交给 LogoutSuccessHandler来处理。
package com.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 注销登录
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
/**Json转化工具*/
@Autowired
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
JwtUser userDetails = (JwtUser)authentication.getPrincipal();
Map<String,String> map = new LinkedHashMap<>();
map.put("code", String.valueOf(HttpServletResponse.SC_OK));
map.put("msg", "退出成功");
map.put("username", userDetails.getUsername());
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
try {
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}catch (IOException o){
o.printStackTrace();
if(writer != null){
writer.flush();
writer.close();
}
}
}
}
四、在用户的使用UsernamePasswordAuthenticationFilter 之前还需要实现UserDetailsService(根据用户的名查询用户信息,校验用户信息准备,与DB交互)
package com.security.service;
import com.security.model.JwtUser;
import com.security.model.UserModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Component;
import java.util.ArrayList;
import java.util.Collection;
/**
* 查询用户关联角色信息
*/
@Component
public class JwtUserDetailsService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(JwtUserDetailsService.class);
// @Autowired
// private UserMapper userMapper;
// @Autowired
// private SysRoleMapper sysRoleMapper;
// @Autowired
// private SysUserRoleMapper sysUserRoleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里只是测试使用,暂时不跟数据库交互了
UserModel user = new UserModel();
user.setUsername(username);
user.setPassword("admin");
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
//此处将权限信息添加到 GrantedAuthority 对象中,在后面进行权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
JwtUser jwtUser = new JwtUser(user.getUsername(),user.getPassword(), grantedAuthorities);
return jwtUser;
}
}
五、过滤器(Filter)。
- 自定义过滤器UsernamePasswordAuthenticationFilter 组装成security上下文中使用的Authentication
- AbstractUserDetailsAuthenticationProvider验证用户信息
- OncePerRequestFilter过滤器保证拦截一次(验证token)
这里可以有两种登录方式:
- form表单登录【spring默认的】
- json登录需要在securityConfig添加自定义的MyUsernamePasswordAuthenticationFilter ,继承AbstractUserDetailsAuthenticationProvider作为登录拦截,然后在securityConfig的配置文件中引入到配置文件中使用authenticationManage重用WebSecurityConfigurerAdapter配置的AuthenticationManager配置自定义的usernamePasswordAuththenticationFilter过滤器。(使用自定义登录)
package com.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.AuthenticationModel;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
/**
* 登录使用自定义的登录(JSON登录)
*/
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//attempt Authentication when Content-Type is json
if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
//use jackson to deserialize json
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()){
AuthenticationModel authenticationBean = mapper.readValue(is,AuthenticationModel.class);
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.getUsername(), authenticationBean.getPassword());
}catch (IOException e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken(
"", "");
}finally {
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
//transmit it to UsernamePasswordAuthenticationFilter
else {
return super.attemptAuthentication(request, response);
}
}
}
使用AbstractUserDetailsAuthenticationProvider 拦截验证用户信息,主要实现了AuthenticationProvider的接口方法 authenticate 并提供了相关的验证逻辑。也可以使用父类AuthenticationProvider
package com.security.filter;
import com.alibaba.druid.util.StringUtils;
import com.security.model.JwtUser;
import com.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义拦截
* 用户登录验证用户信息
*/
@Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {//implements AuthenticationProvider {
@Autowired
private JwtUserDetailsService userDetailsService;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//用户输入的用户名
String username = String.valueOf(authentication.getName());
//用户输入的密码
String password = String.valueOf(authentication.getCredentials());
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
throw new BadCredentialsException("用户名或密码为空!");
}
//通过自定义的CustomUserDetailsService,以用户输入的用户名查询用户信息
JwtUser userDetails = (JwtUser) userDetailsService.loadUserByUsername(username);
//用户输入密码加密后与数据库比较
BCryptPasswordEncoder encode = new BCryptPasswordEncoder();
if(!encode.matches(password,userDetails.getPassword())){
throw new BadCredentialsException("密码错误!");
}
Object principalToReturn = userDetails;
//将用户信息塞到SecurityContext中,方便获取当前用户信息 把当前用户信息放入Security全局缓存中
return this.createSuccessAuthentication(principalToReturn, authentication, userDetails);
}
@Override
protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
OncePerRequestFilter过滤器保证拦截一次(验证token)
package com.demo.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.demo.security.service.JwtUserDetailsService;
import com.demo.security.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 确保经过filter为一次请求
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final static String HEADER = "Authorization";
private final static String BEARER = "Bearer ";
public static String[] arrUrl = new String[]{"/user/login"};
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean boo = false;
for(int i = 0; i < arrUrl.length; i++){
if(antPathMatcher.match(arrUrl[i],request.getRequestURI())){
boo = true;
break;
}
}
if(boo){
chain.doFilter(request, response);
return;
}
String header = request.getHeader(HEADER);
if (header == null || !header.startsWith(BEARER)) {
getResponse(response,"token不合法!");
return;
}
final String authToken = header.substring(BEARER.length());
if(JwtTokenUtil.isTokenExpired(authToken)){
getResponse(response,"token过期!");
return;
}
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if(username == null || username == ""){
getResponse(response,"token错误!");
return;
}
//把用户的信息填充到上下文中
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
logger.info("checking authentication " + username);
chain.doFilter(request, response);
}
/**
* 组装token验证失败的返回
* @param res
* @param msg
* @return
*/
private HttpServletResponse getResponse(HttpServletResponse res,String msg){
Map<String,String> map = new LinkedHashMap<>();
map.put("code", String.valueOf(HttpServletResponse.SC_FORBIDDEN));
map.put("msg", msg);
res.setContentType("Application/json;charset=UTF-8");
Writer writer;
try {
writer = res.getWriter();
writer.write(objectMapper.writeValueAsString(map));
writer.flush();
writer.close();
}catch (Exception o){
o.printStackTrace();
}
return res;
}
}
六、RBAC自定义url的验证权限
package com.demo.security.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service("rbacauthorityservice")
public class RbacAuthorityService {
// @Autowired
// private SysResourceMapper sysResourceMapper;
/**
* 自定义权限信息
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities());
//得到的principal的信息是用户名还是整个用户信息取决于在SelfAuthenticationProvider中传参的方式
Object userInfo = authentication.getPrincipal();
boolean hasPermission = false;
if (userInfo instanceof UserDetails) {
String username = ((UserDetails) userInfo).getUsername();
//这里不做数据库菜单路径的交互
List<String> list = new ArrayList<>();
list.add("/index");
list.add("/system");
list.add("/user");
//自定义验证规则
//获取当前用户的权限菜单,和请求的菜单路径做匹配
for (int i = 0; i < list.size(); i++) {
String role = "/api/v*" + list.get(i) + "/**";
AntPathMatcher antPathMatcher = new AntPathMatcher();
if (antPathMatcher.match(role, request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
}
return hasPermission;
}
}
代码已经上传到git:https://github.com/lulu0008/spring-security.git