技术背景

到目前为止,我们使用的权限认证框架是 Shiro,虽然 Shiro 也足够好用并且简单,但对于 Spring 官方主推的安全框架 Spring Security,用户群也是甚大的,所以我们这里把当前的代码切分出一个 shiro-cloud 分支,作为 Shiro + Spring Cloud 技术的分支代码,dev 和 master 分支将替换为 Spring Security + Spring Cloud 的技术栈,并在后续计划中集成 Spring Security OAuth2 实现单点登录功能。

代码实现

Maven依赖

移除shiro依赖,添加Spring Scurity和JWT依赖包,jwt目前的最新版本是0.9.1。

<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>

权限注解

替换Shiro的权限注解为Spring Security的权限注解。

格式如下:

@PreAuthorize("hasAuthority('sys:menu:view')")

SysMenuController.java

package com.louis.kitty.admin.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import com.louis.kitty.admin.model.SysMenu;
import com.louis.kitty.admin.sevice.SysMenuService;
import com.louis.kitty.core.http.HttpResult; /**
* 菜单控制器
* @author Louis
* @date Oct 29, 2018
*/
@RestController
@RequestMapping("menu")
public class SysMenuController { @Autowired
private SysMenuService sysMenuService; @PreAuthorize("hasAuthority('sys:menu:add') AND hasAuthority('sys:menu:edit')")
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysMenu record) {
return HttpResult.ok(sysMenuService.save(record));
} @PreAuthorize("hasAuthority('sys:menu:delete')")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysMenu> records) {
return HttpResult.ok(sysMenuService.delete(records));
} @PreAuthorize("hasAuthority('sys:menu:view')")
@GetMapping(value="/findNavTree")
public HttpResult findNavTree(@RequestParam String userName) {
return HttpResult.ok(sysMenuService.findTree(userName, 1));
} @PreAuthorize("hasAuthority('sys:menu:view')")
@GetMapping(value="/findMenuTree")
public HttpResult findMenuTree() {
return HttpResult.ok(sysMenuService.findTree(null, 0));
}
}

Spring Security注解默认是关闭的,可以通过在配置类添加以下注解开启。

@EnableGlobalMethodSecurity(prePostEnabled = true)

安全配置

添加安全配置类, 继承 WebSecurityConfigurerAdapter,配置URL验证策略和相关过滤器以及自定义的登录验证组件。

WebSecurityConfig.java

package com.louis.kitty.admin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import com.louis.kitty.admin.security.JwtAuthenticationFilter;
import com.louis.kitty.admin.security.JwtAuthenticationProvider; /**
* Spring Security Config
* @author Louis
* @date Nov 20, 2018
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private UserDetailsService userDetailsService; @Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
} @Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// web jars
.antMatchers("/webjars/**").permitAll()
// 查看SQL监控(druid)
.antMatchers("/druid/**").permitAll()
// 首页和登录页面
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 验证码
.antMatchers("/captcha.jpg**").permitAll()
// 服务监控
.antMatchers("/actuator/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// 登录认证过滤器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
} @Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
} }

登录验证组件

继承 DaoAuthenticationProvider, 实现自定义的登录验证组件,覆写密码验证逻辑。

JwtAuthenticationProvider.java

package com.louis.kitty.admin.security;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import com.louis.kitty.admin.util.PasswordEncoder; /**
* 身份验证提供者
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationProvider extends DaoAuthenticationProvider { public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
} @Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails) userDetails).getSalt();
// 覆写密码验证逻辑
if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
} }

用户认证信息查询组件

实现 UserDetailsService 接口,定义用户认证信息查询组件,用于获取认证所需的用户信息和授权信息。

UserDetailsServiceImpl.java

package com.louis.kitty.admin.security;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 com.louis.kitty.admin.model.SysUser;
import com.louis.kitty.admin.sevice.SysUserService; /**
* 用户登录认证信息查询
* @author Louis
* @date Nov 20, 2018
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired
private SysUserService sysUserService; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = sysUserService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
}
}

用户认证信息封装

上面 UserDetailsService 查询的信息需要封装到实现 UserDetails 接口的封装对象里。

JwtUserDetails.java

package com.louis.kitty.admin.security;
import java.util.Collection; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import com.fasterxml.jackson.annotation.JsonIgnore; /**
* 安全用户模型
* @author Louis
* @date Nov 20, 2018
*/
public class JwtUserDetails implements UserDetails { private static final long serialVersionUID = 1L; private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities; JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
} @Override
public String getUsername() {
return username;
} @JsonIgnore
@Override
public String getPassword() {
return password;
} public String getSalt() {
return salt;
} @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
} @JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
} @JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
} @JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
} @JsonIgnore
@Override
public boolean isEnabled() {
return true;
} }

登录接口

因为我们没有使用内置的 formLogin 登录处理过滤器,所以需要调用登录认证流程,修改登录接口,加入系统登录认证调用。

SysLoginController.java

   /**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();...
     // 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); return HttpResult.ok(token);
}

Spring Security 的登录认证过程是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的。

登录流程中主要是返回一个认证好的 Authentication 对象,然后保存到上下文供后续进行授权的时候使用。

登录认证成功之后,会利用JWT生成 token 返回给客户端,后续的访问都需要携带此 token 来进行认证。

SecurityUtils.java

    /**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}

令牌生成器

令牌生成器主要是利用JWT生成所需的令牌,部分代码如下。

JwtTokenUtils.java

/**
* JWT工具类
* @author Louis
* @date Nov 20, 2018
*/
public class JwtTokenUtils implements Serializable { /**
* 生成令牌
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
} /**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
}

登录认证过滤器

登录认证过滤器继承 BasicAuthenticationFilter,在访问任何URL的时候会被此过滤器拦截,通过调用 SecurityUtils.checkAuthentication(request) 检查登录状态。

JwtAuthenticationFilter.java

package com.louis.kitty.admin.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import com.louis.kitty.admin.util.SecurityUtils; /**
* 登录认证过滤器
* @author Louis
* @date Nov 20, 2018
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
} @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
} }

登录认证检查

登录验证检查是通过 SecurityUtils.checkAuthentication(request) 来完成的。

SecurityUtils.java

    /**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}

上面的登录验证是通过 JwtTokenUtils.getAuthenticationeFromToken(request),来验证令牌并返回登录信息的。

JwtTokenUtils.java

    /**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 请求令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {
return null;
}
String username = claims.getSubject();
if(username == null) {
return null;
}
if(isTokenExpired(token)) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}

清除Shiro配置

清除掉 config 包下的 ShiroConfig 配置类。

清除 oautho2 包下有关 Shiro 的相关代码。

清除掉 sys_token 表和相关操作代码。

Spring Boot + Spring Cloud 实现权限管理系统 (Spring Security 版本 )的更多相关文章

  1. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(一):Kitty 系统介绍

    在线演示 演示地址:http://139.196.87.48:9002/kitty 用户名:admin 密码:admin 温馨提示: 有在演示环境删除数据的童鞋们,如果可以的话,麻烦动动小指,右键头像 ...

  2. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)

    技术背景 上一篇教程中,我们利用Consul注册中心,实现了服务的注册和发现功能,这一篇我们来聊聊服务的调用.单体应用中,代码可以直接依赖,在代码中直接调用即可,但在微服务架构是分布式架构,服务都运行 ...

  3. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(七):集成 Druid 数据源

    数据库连接池负责分配.管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个:释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏 ...

  4. 基于Spring Boot和Shiro的后台管理系统FEBS

    FEBS是一个简单高效的后台权限管理系统.项目基础框架采用全新的Java Web开发框架 —— Spring Boot 2.0.3,消除了繁杂的XML配置,使得二次开发更为简单:数据访问层采用Myba ...

  5. spring boot、cloud v2.1.0.RELEASE 使用及技术整理

    2018年10月30日 springboot v2.1.0.RELEASE 发布: https://github.com/spring-projects/spring-boot/releases/ta ...

  6. Spring Boot:整合Shiro权限框架

    综合概述 Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证.授权.加密.会话管理等功能,与Spring Security一样属基于权限的安全框架,但是与Sprin ...

  7. spring boot 2 + shiro 实现权限管理

    Shiro是一个功能强大且易于使用的Java安全框架,主要功能有身份验证.授权.加密和会话管理.看了网上一些文章,下面2篇文章写得不错.Springboot2.0 集成shiro权限管理 Spring ...

  8. Spring Boot集成Shrio实现权限管理

    Spring Boot集成Shrio实现权限管理   项目地址:https://gitee.com/dsxiecn/spring-boot-shiro.git   Apache Shiro是一个强大且 ...

  9. Spring Boot的前世今生以及它和Spring Cloud的关系详解。

    要了解Spring Boot的发展背景,还得从2004年Spring Framework1.0版本发布开始说起,不过大家都是从开始学习Java就使用Spring Framework了,所以就不做过多展 ...

  10. spring boot 2.0(一)权威发布spring boot2.0

    Spring Boot2.0.0.RELEASE正式发布,在发布Spring Boot2.0的时候还出现一个小插曲,将Spring Boot2.0同步到Maven仓库的时候出现了错误,然后Spring ...

随机推荐

  1. C工程 交互 ceph 分布式存储系统

    网上看到有人问,如何在C项目里调用ceph系统对外提供的API,实现分布式存储. 我在网上搜到了相关信息,但是因为不是会员无法追加答案,故而,贴于此. 赠予有缘人:) ———————————————— ...

  2. arch----------arch下的一些命令,亲测

    1.taoyanghao 不在 sudoers 文件中.此事将被报告. 这个是使用sudo以后报出的错误提示,sudo确定已经安装了. 解决方案:编辑/etc/sudoers文件.找到这一 行:&qu ...

  3. Filte过滤器

    过滤器 , 其实就是对客户端发出来的请求进行过滤. 浏览器发出, 然后服务器派servlet处理.  在中间就可以过滤, 其实过滤器起到的是拦截的作用.生活中的过滤器例如:门禁,你有门卡才能过去,没有 ...

  4. caffe运行报错:datum channel>0(0:0)

    caffe在运行的时候报错:datum channel>0(0:0) 错误原因:数据通道错误,caffe不能识别 解决方案:不告诉你

  5. 133. Clone Graph(图的复制)

    Given the head of a graph, return a deep copy (clone) of the graph. Each node in the graph contains ...

  6. IRC 打字交流

    kali 里面用 apt-get install weechat 安装完成后,输入 weechat 命令就能启动客户端了 要想使用 IRC,就需要先连接一个 irc 服务器,选择了大名鼎鼎的 chat ...

  7. QT使用MSVC编译器输出中文乱码问题解决方法

    方法一:使用QStringLiteral()宏对每个中文字符串封装,此方法的缺点是不能在使用tr()函数用于字符串翻译: 方法二:强制MSVC编译器采用UTF-8编码生成可执行文件,需要在出现中文字符 ...

  8. Python Redis 管道

    redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pi ...

  9. iOS开发 -------- AFNetworking实现简单的断点下载

    一 实现如下效果   二 实现代码 // // ViewController.m // AFNetworking实现断点下载 // // Created by lovestarfish on 15/1 ...

  10. react-router 4.0(四)跳转404

    import React from 'react' import ReactDOM from 'react-dom' import { HashRouter, Route, Link, Prompt, ...