技术背景

到目前为止,我们使用的权限认证框架是 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. Hbase 读写 原理

    客户端读取信息流程 ()client要读取信息,先查询下client 端的cache中是否存在数据,如果存在,刚直接返回数据.如果不存在,则进入到zookeeper,查找到里面的相应数据存在的Root ...

  2. git之概念图

    1.git四大区. . 2. 3. 4.

  3. 查看 chrome 浏览器中的 Headers

    查看 chrome 浏览器中的 Headers, Response 信息:

  4. 主线程——main线程

    定义一个普通的类: 引用这个类,执行main方法,main方法就是一个主线程: 线程:进程的执行单元,可以理解为栈内存中的所执行的方法(除了main方法之外都是线程中的run方法)地址开辟通往cpu的 ...

  5. (Review cs231n) Optimized Methods

    Mini-batch SGD的步骤: 1.Sample a batch of data 2.Forward prop it through the graph,get loss 3.backprop ...

  6. Ajax跨域请求COOKIE无法带上的解决办法

    原生ajax请求方式: var xhr = new XMLHttpRequest(); xhr.open("POST", "http://xxxx.com/demo/b/ ...

  7. k-means算法 - 数据挖掘算法(5)

    (2017-05-02 银河统计) k-means算法,也被称为k-平均或k-均值,是数据挖掘技术中一种广泛使用的聚类算法. 它是将各个聚类子集内的所有数据样本的均值作为该聚类的代表点,算法的主要思想 ...

  8. Linux+Jenkins环境搭建

    一.安装基础环境 1. yum -y install java-1.8.0-openjdk.x86_64 #安装1.8jdk 2. 查看java 版本 [root@localhost djanggo_ ...

  9. js实现复制内容到剪切板,兼容pc和手机端,支持Safari浏览器

    Javascript原生有一些事件:copy.paste.cut, 这些事件可以作用的目标元素: 能获得焦点的元素 (如contentEditable内容能编辑或者可以选中的元素),或者是<bo ...

  10. c语言格式化打印

    printf的格式化打印 unsigned int          -------------------     %u        打印无符号数 int          ----------- ...