安全框架(springsecurity、shiro等)主要分为两个部分:
- 认证:系统认为用户是否能登录
- 授权:系统判断用户是否有权限去做某些事情
项目主要使用了:基于token的用户权限认证与授权
- 模块一:登录时springsecurity获取用户的信息,比如用户名、密码、和查数据库得到其权限列表
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式
进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限
值,并以用户名为 key ,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息
生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带
到 header 请求头中, Spring-security 解析 header 头获取 token 信息,解析 token 获取当前
用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前
请求是否有权限访问
项目后台系统权限管理模块说明
- 用户管理:表示可以登录这个后台网站的用户
- 角色管理:不同的角色可以访问不同的权限菜单集合,要给每个用户分配一种角色
权限管理数据库模型:
菜单表acc_permission
- 一共有两级菜单,一级菜单的pid为1,二级菜单的pid为一级菜单的id
菜单角色关系表acl_role_permission
- 维护菜单和角色的关系
用户角色关系表acl_user_role
- 维护用户和角色之间的关系
JWT工具类
- 项目中用户登录成功后,需要根据用户名生成token,放在cookie中,jwt就是生成token的工具。当前端给服务器发送请求时,请求头会携带token信息,服务器通过校验token,来确定该用户的权限
- token就是一种服务器颁发的访问令牌,服务器会设置签名算法和密钥,通过签名算法和密钥以及token的有效载荷,生成token的签名,保证数据不会被篡改。
- 签名哈希
注意在权限管理模块使用的JWT工具类是:但是这个工具类比较简单,我就拿common_utils中的JWT工具类来说明!!!!!!
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24;//设置token的过期时间
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";//秘钥加密
//生成token字符串的方法(根据用户的id和昵称生成字符串)
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")//设置令牌类型为JWT令牌
.setHeaderParam("alg", "HS256")//设置签名算法为HS256
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))//设置token的过期时间
//claim是token的主体部分
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET) //设置签名算法和密钥
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");//把token放在request的头信息中
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @param request
* @return
*/
//解析token,得到用户id
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");//从请求头中获取token字符串
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}
springsecurity4个工具类(这一部分具体直接看代码注释)
认证、授权过滤器(具体看代码注释)
- 认证过滤器
认证的过程是,先从登录请求中拿到对应的用户名和密码,对用户名密码进行登录校验(与数据库中比对),认证成功后执行successfulAuthentication方法,通过JWT工具类根据用户名生成token放在客户端的cookie中然后在redis中加入一个key,value,分别是用户名和权限列表认证失败则在响应中返回错误信息。 - 授权过滤器
当一个用户已经登录后,下次再访问服务器时,请求头中包含token信息,
服务器先从header中获取token,解析得到用户名,从redis中获取权限列表,由springsecurity给当前用户赋予权限*(哪些可以访问,哪些不能访问)
- 使用redisTemplate操作redis
service_acl模块
UserDetailsServiceImpl类
- 因为在springsecurity包的核心配置类中要使用userDetailsService接口的实现类来操作数据库,查询用户的用户名密码等数据
- 因此 service_acl模块中要有这个实现类——UserDetailsServiceImpl类
@Service("userDetailsService")//该bean对象的引用名就是userDetailsService
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/***
* 根据账号获取用户信息
* @param username:
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中取出用户信息
User user = userService.selectByUsername(username);
// 判断用户是否存在
if (null == user){
//throw new UsernameNotFoundException("用户名不存在!");
}
// 返回UserDetails实现类
com.atguigu.serurity.entity.User curUser = new com.atguigu.serurity.entity.User();
BeanUtils.copyProperties(user,curUser);
//查询用户的权限列表
List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser(curUser);
securityUser.setPermissionValueList(authorities);
return securityUser;
}
}
service_acl其他部分:
- 其他部分主要是对这五张表进行增删改查操作
api_gateway模块:
CorsConfig:解决跨域问题
@Configuration
public class CorsConfig {
//解决跨域问题,有这个类后,其他模块的controller就不用加@CrossOrigin注解了
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
具体跨域的问题,看下面三个博客
springboot解决跨域和集成springsecurity解决跨域
application.properties:
# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
#下面都是gateway配置
#使用服务发现路由(开启nacos服务发现)
spring.cloud.gateway.discovery.locator.enabled=true
#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri lb://nacos注册服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**
#配置service-ucenter服务
spring.cloud.gateway.routes[2].id=service-ucenter
spring.cloud.gateway.routes[2].uri=lb://service-ucenter
spring.cloud.gateway.routes[2].predicates= Path=/ucenterservice/**
#配置service-ucenter服务
spring.cloud.gateway.routes[3].id=service-cms
spring.cloud.gateway.routes[3].uri=lb://service-cms
spring.cloud.gateway.routes[3].predicates= Path=/cmsservice/**
spring.cloud.gateway.routes[4].id=service-msm
spring.cloud.gateway.routes[4].uri=lb://service-msm
spring.cloud.gateway.routes[4].predicates= Path=/edumsm/**
spring.cloud.gateway.routes[5].id=service-order
spring.cloud.gateway.routes[5].uri=lb://service-order
spring.cloud.gateway.routes[5].predicates= Path=/orderservice/**
spring.cloud.gateway.routes[6].id=service-order
spring.cloud.gateway.routes[6].uri=lb://service-order
spring.cloud.gateway.routes[6].predicates= Path=/orderservice/**
spring.cloud.gateway.routes[7].id=service-oss
spring.cloud.gateway.routes[7].uri=lb://service-oss
spring.cloud.gateway.routes[7].predicates= Path=/eduoss/**
spring.cloud.gateway.routes[8].id=service-statistic
spring.cloud.gateway.routes[8].uri=lb://service-statistic
spring.cloud.gateway.routes[8].predicates= Path=/staservice/**
spring.cloud.gateway.routes[9].id=service-vod
spring.cloud.gateway.routes[9].uri=lb://service-vod
spring.cloud.gateway.routes[9].predicates= Path=/eduvod/**
spring.cloud.gateway.routes[10].id=service-edu
spring.cloud.gateway.routes[10].uri=lb://service-edu
spring.cloud.gateway.routes[10].predicates= Path=/eduservice/**
gateway、nacos和权限管理模块的关系
- 包含acl的请求路径会被网关拦截,然后去注册中心找对应的服务名,再转发到对应的权限管理模块中去
从上面配置文件看出,网关服务(api_gateway)对外暴露的端口是8222,也就是说前端的请求url是网关的ip+8222
网关接收到请求后,网关根据请求url的字符信息,去注册中心发现对应的服务(服务发现)。
前提是各个服务已经在nacos注册中心注册了
具体的nacos服务发现的规则(也就是怎么根据请求url找到对应的nacos注册中心的服务名)由上面的配置文件规定
和nginx的配置文件中配置的是一样的意思
springboot本质上是一个过滤器链,也就是说提供很多过滤器,这里只详细介绍项目中用到的过滤器。
UsernamePasswordAuthenticationFilter(登录过滤器)
- 项目中TokenLoginFilter继承了这个类
- 作用:对/login登录的post请求做拦截,校验表单中的用户名和密码
/**
* <p>
* 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
* </p>
*
* @author qy
* @since 2019-11-08
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
- 打开UsernamePasswordAuthenticationFilter源码,有下面这个方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//1、先判断是不是post提交
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//2、从请求request获取用户名和密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//3、查数据库,对用户名和密码进行校验
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 先判断是不是post提交
- 从请求request获取用户名和密码
- 查数据库,对用户名和密码进行校验
过滤器是如何被加载的?
- springboot帮我们完成了springsecurity的自动化配置
两个重要的接口:
UserDetails
- 项目中SecurityUser实现了这个接口
- SecurityUser
/**
* <p>
* 安全认证用户详情信息
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
// 表示获取登录用户所有权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
// 表示获取密码
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
// 表示获取用户名
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
// 表示判断账户是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 表示判断账户是否被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 表示凭证{密码}是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 表示当前用户是否可用
@Override
public boolean isEnabled() {
return true;
}
}
- UserDetails 源码:
public interface UserDetails extends Serializable {
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
}
- 简而言之,UserDetails就是用来获取用户的一些具体信息的
PasswordEncoder
- 密码需要加密,不是明文
- 项目中DefaultPasswordEncoder实现了这个接口
/**
* <p>
* t密码的处理方法类型
* </p>
*
* @author qy
* @since 2019-11-08
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
/**
* @param strength
* the log rounds to use, between 4 and 31
*/
public DefaultPasswordEncoder(int strength) {
}
//使用MD5加密密码
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
- 项目中通过MD5算法加密
- matches方法: 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹 配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
springsecurity如何实现认证和授权(源码级)
SpringSecurity 的过滤器介绍
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信 息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在
Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和
AuthenticationException 异常。
(14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
- SpringSecurity 基本流程
Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在配置类的configure方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:
UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。
ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由 ExceptionTranslationFilter 过滤器进行捕获和处理。
SpringSecurity 认证流程
认证流程是在 UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下所示:
- 查看 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter,有一个doFilter方法,认证过程主要都在这个方法里
- 1、先判断提交方式是不是post,是的话进入下一个过滤器,不是的话就放行
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
- 2、 doFilter方法会调用其子类的attemptAuthentication方法进行认证,其子类的attemptAuthentication方法首先获取表单提交的用户名和密码并封装到authRequest对象中,再交给getAuthenticationManager().authenticate来处理。getAuthenticationManager().authenticate这个方法里面是调用userDetailsService接口的实现类(在service_acl模块中)来查询数据库的方法来进行身份认证!!
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
3、认证失败就去异常中处理,执行认证失败的方法:this.unsuccessfulAuthentication(request, response, var8);
4、认证成功就执行认证成功的方法:this.successfulAuthentication(request, response, chain, authResult);