分布式项目和传统项目的区别就是,分布式项目有多个服务,每一个服务仅仅只实现一套系统中一个或几个功能,所有的服务组合在一起才能实现系统的完整功能。这会产生一个问题,多个服务之间session不能共享,你在其中一个服务中登录了,登录信息保存在这个服务的session中,别的服务不知道啊,所以你访问别的服务还得在重新登录一次,对用户十分不友好。为了解决这个问题,于是就产生了单点登录:
**jwt单点登录:**就是用户在登录服务登录成功后,登录服务会产生向前端响应一个token(令牌),以后用户再访问系统的资源的时候都要带上这个令牌,各大服务对这个令牌进行验证(令牌是否过期,令牌是否被篡改),验证通过了,可以访问资源,同时,令牌中也会携带一些不重要的信息,比如用户名,权限。通过解析令牌就能知道当前登录的用户和用户所拥有的权限。
下面我们就来写一个案例项目看看具体如何使用
1 创建项目结构1.1 父工程cloud-security
这是父工程所需要的包
org.springframework.boot spring-boot-starter-parent2.1.3.RELEASE org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-securityorg.springframework.boot spring-boot-starter-test
1.2 公共工程 security-common
这是公共工程所需要的包
org.projectlombok lombokcom.alibaba fastjson1.2.60 io.jsonwebtoken jjwt-api0.11.2 io.jsonwebtoken jjwt-impl0.11.2 runtime io.jsonwebtoken jjwt-jackson0.11.2 runtime
1.3 认证服务security-sever
这个服务仅仅只有两项功能:
(1)用户登录,颁发令牌
(2)用户注册
我们这里只实现第一个功能
1.3.1 认证服务所需的包
cn.lx.security security-common1.0-SNAPSHOT mysql mysql-connector-javatk.mybatis mapper-spring-boot-starter2.0.4 org.springframework.boot spring-boot-starter-thymeleaf
1.3.2 配置application.yml
这里面的配置没什么好说的,都很简单
server: port: 8080 spring: datasource: url: jdbc:mysql:///security_authority?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false main: allow-bean-definition-overriding: true mybatis: type-aliases-package: cn.lx.security.doamin configuration: #驼峰 map-underscore-to-camel-case: true logging: level: cn.lx.security: debug
1.3.3 导入domain,dao,service,config
这个可以在上篇文档中找到,我们只需要service中的loadUserByUsername方法及其所调用dao中的方法
完整项目在我的github中,地址:git@github.com:lx972/cloud-security.git
配置文件我们也从上篇中复制过来MvcConfig,SecurityConfig
1.3.4 测试
访问http://localhost:8080/loginPage成功出现登录页面,说明认证服务的骨架搭建成功了
1.4 资源服务security-resource1
实际项目中会有很多资源服务,我只演示一个
为了简单,资源服务不使用数据库
1.4.1 资源服务所需的包
cn.lx.security security-common1.0-SNAPSHOT
1.4.2 配置application.yml
server: port: 8090 logging: level: cn.lx.security: debug
1.4.3 controller
拥有ORDER_LIST权限的才能访问
@RestController
@RequestMapping("/order")
public class OrderController {
//@Secured("ORDER_LIST")
@PreAuthorize(value = "hasAuthority('ORDER_LIST')")
@RequestMapping("/findAll")
public String findAll(){
return "order-list";
}
}
拥有PRODUCT_LIST权限的才能访问
@RestController
@RequestMapping("/product")
public class ProductController {
//@Secured("PRODUCT_LIST")
@PreAuthorize(value = "hasAuthority('PRODUCT_LIST')")
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
1.4.4 security配置类
@Configuration
@EnableWebSecurity
//这个注解先不要加
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated();
}
}
1.4.5 测试
访问http://localhost:8090/order/findAll成功打印出order-list,服务搭建成功。
2 认证服务实现登录,颁发令牌首先,我们必须知道我们的项目是前后端分离的项目,所以我们不能由后端控制页面跳转了,只能返回json串通知前端登录成功,然后前端根据后端返回的信息控制页面跳转。
2.1 登录成功或者登录失败后的源码分析
UsernamePasswordAuthenticationFilter中登录成功后走successfulAuthentication方法
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//将已通过认证的Authentication保存到securityContext容器中,应为后面的过滤器需要使用
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//这个方法你点进去,就会发现,真正作业面跳转是在这里
successHandler.onAuthenticationSuccess(request, response, authResult);
}
UsernamePasswordAuthenticationFilter中登录成功后走unsuccessfulAuthentication方法
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//记住我失败
rememberMeServices.loginFail(request, response);
//失败后的页面跳转都在这里
failureHandler.onAuthenticationFailure(request, response, failed);
}
2.2 重写successfulAuthentication和unsuccessfulAuthentication方法
我们继承UsernamePasswordAuthenticationFilter这个过滤器
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public AuthenticationFilter(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//认证成功的对象放入securityContext容器中
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//创建令牌
Map claims=new HashMap<>();
SysUser sysUser = (SysUser) authResult.getPrincipal();
claims.put("username",sysUser.getUsername());
claims.put("authorities",authResult.getAuthorities());
//这个方法在下面介绍
String jwt = JwtUtil.createJwt(claims);
//直接返回json
ResponseUtil.responseJson(new Result("200", "登录成功",jwt),response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//清理容器中保存的认证对象
SecurityContextHolder.clearContext();
//直接返回json
ResponseUtil.responseJson(new Result("500", "登录失败"),response);
}
}
2.3 令牌创建
String jwt = JwtUtil.createJwt(claims);
这个方法干了什么事呢
public static String createJwt(Mapclaims){ //获取私钥 String priKey = KeyUtil.readKey("privateKey.txt"); //将string类型的私钥转换成PrivateKey,jwt只能接受PrivateKey的私钥 PKCS8EncodedKeySpec priPKCS8 = null; try { priPKCS8 = new PKCS8EncodedKeySpec(new base64Decoder().decodeBuffer(priKey)); KeyFactory keyf = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyf.generatePrivate(priPKCS8); //创建令牌 String jws = Jwts.builder() //设置令牌过期时间30分钟 .setExpiration(new Date(System.currentTimeMillis()+1000*60*30)) //为令牌设置额外的信息,这里我们设置用户名和权限,还可以根据需要继续添加 .addClaims(claims) //指定加密类型为rsa .signWith(privateKey, SignatureAlgorithm.RS256) //得到令牌 .compact(); log.info("创建令牌成功:"+jws); return jws; } catch (Exception e) { throw new RuntimeException("创建令牌失败"); } }
获取秘钥的方法
public class KeyUtil {
public static String readKey(String keyName){
//文件必须放在resources根目录下
ClassPathResource resource=new ClassPathResource(keyName);
String key =null;
try {
InputStream is = resource.getInputStream();
key = StreamUtils.copyToString(is, Charset.defaultCharset());
}catch (Exception e){
throw new RuntimeException("读取秘钥错误");
}
if (key==null){
throw new RuntimeException("秘钥为空");
}
return key;
}
}
2.4 响应json格式数据给前端
封装成了一个工具类
public class ResponseUtil {
public static void responseJson(Result result, HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(200);
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSonString(result));
writer.flush();
writer.close();
}
}
返回结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private String code;
private String msg;
private Object data;
public Result(String code, String msg) {
this.code = code;
this.msg = msg;
}
}
3 认证服务实现令牌验证和解析
除了security配置类中配置的需要忽略的请求之外,其他所有请求必须验证请求头中是否携带令牌,没有令牌直接响应json数据,否则就验证和解析令牌。
security中有一个过滤器是实现令牌BasicAuthenticationFilter认证的,只不过他是basic的,没关系,我们继承它,然后重写解析basic的方法
3.1 源码分析
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
//获取请求头中Authorization的值
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
//值不符合条件直接放行
chain.doFilter(request, response);
return;
}
try {
//就是解析Authorization
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
//tokens[0]用户名 tokens[1]密码
String username = tokens[0];
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
//判断是否需要认证(容器中有没有该认证对象)
if (authenticationIsRequired(username)) {
//创建一个对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
//进行认证,我们不关心它如何认证,我们需要按自己的方法对令牌认证解析
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
//已认证的对象保存到securityContext中
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
3.2 重写doFilterInternal方法
继承BasicAuthenticationFilter
public class TokenVerifyFilter extends BasicAuthenticationFilter {
public TokenVerifyFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("bearer ")) {
//直接返回json
ResponseUtil.responseJson(new Result("403", "用户未登录"),response);
return;
}
//得到jwt令牌
String jwt = StringUtils.replace(header, "bearer ", "");
//解析令牌
String[] tokens = JwtUtil.extractAndDecodeJwt(jwt);
//用户名
String username = tokens[0];
//权限
List authorities= JSON.parseArray(tokens[1], SysPermission.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username,
null,
authorities
);
//放入SecurityContext容器中
SecurityContextHolder.getContext().setAuthentication(authRequest);
chain.doFilter(request, response);
}
}
3.3 验证解析令牌
public static String decodeJwt(String compactJws){
//获取公钥
String pubKey = KeyUtil.readKey("publicKey.txt");
//将string类型的私钥转换成PublicKey,jwt只能接受PublicKey的公钥
KeyFactory keyFactory;
try {
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(
new base64Decoder().decodeBuffer(pubKey));
keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(bobPubKeySpec);
Claims body = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(compactJws).getBody();
String jwtString = JSON.toJSonString(body);
//OK, we can trust this JWT
log.info("解析令牌成功:"+jwtString);
return jwtString;
} catch (Exception e) {
throw new RuntimeException("解析令牌失败");
}
}
public static String[] extractAndDecodeJwt(String compactJws){
//获取令牌的内容
String decodeJwt = decodeJwt(compactJws);
JSonObject jsonObject = JSON.parseObject(decodeJwt);
String username = jsonObject.getString("username");
String authorities = jsonObject.getString("authorities");
return new String[] { username, authorities };
}
3.4 修改security配置类
将自定义过滤器加入过滤器链
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IUserService iUserService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
AuthenticationManager authenticationManager = super.authenticationManagerBean();
return authenticationManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中注册一个账号
//auth.inMemoryAuthentication().withUser("user").password("{noop}123").roles("USER");
//连接数据库,使用数据库中的账号
auth.userDetailsService(iUserService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterAt(new AuthenticationFilter(super.authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(new TokenVerifyFilter(super.authenticationManager()), BasicAuthenticationFilter.class)
//.formLogin().loginPage("/login.jsp").loginProcessingUrl("/login").defaultSuccessUrl("/index.jsp").failureForwardUrl("/failer.jsp").permitAll()
.formLogin().loginPage("/loginPage").loginProcessingUrl("/login").permitAll()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/loginPage").invalidateHttpSession(true).permitAll();
}
}
4 资源服务实现令牌验证和解析
复制认证服务的TokenVerifyFilter到资源服务
然后修改security的配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
//禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//添加自定义过滤器
.addFilterAt(new TokenVerifyFilter(super.authenticationManager()), BasicAuthenticationFilter.class);
}
}
到此这篇关于spring security在分布式项目下的配置方法(案例详解)的文章就介绍到这了,更多相关spring security分布式内容请搜索考高分网以前的文章或继续浏览下面的相关文章希望大家以后多多支持考高分网!



