spring security在分布式项目下的配置方法(案例详解)

网友投稿 313 2022-11-18


spring security在分布式项目下的配置方法(案例详解)

分布式项目和传统项目的区别就是,分布式项目有多个服务,每一个服务仅仅只实现一套系统中一个或几个功能,所有的服务组合在一起才能实现系统的完整功能。这会产生一个问题,多个服务之间session不能共享,你在其中一个服务中登录了,登录信息保存在这个服务的session中,别的服务不知道啊,所以你访问别的服务还得在重新登录一次,对用户十分不友好。为了解决这个问题,于是就产生了单点登录:

**jwt单点登录:**就是用户在登录服务登录成功后,登录服务会产生向前端响应一个token(令牌),以后用户再访问系统的资源的时候都要带上这个令牌,各大服务对这个令牌进行验证(令牌是否过期,令牌是否被篡改),验证通过了,可以访问资源,同时,令牌中也会携带一些不重要的信息,比如用户名,权限。通过解析令牌就能知道当前登录的用户和用户所拥有的权限。

下面我们就来写一个案例项目看看具体如何使用

1 创建项目结构

1.1 父工程cloud-security

这是父工程所需要的包

org.springframework.boot

spring-boot-starter-parent

2.1.3.RELEASE

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-test

1.2 公共工程 security-common

这是公共工程所需要的包

org.projectlombok

lombok

com.alibaba

fastjson

1.2.60

io.jsonwebtoken

jjwt-api

0.11.2

io.jsonwebtoken

jjwt-impl

0.11.2

runtime

io.jsonwebtoken

jjwt-jackson

0.11.2

runtime

1.3 认证服务security-sever

这个服务仅仅只有两项功能:

(1)用户登录,颁发令牌

(2)用户注册

我们这里只实现第一个功能

1.3.1 认证服务所需的包

cn.lx.security

security-common

1.0-SNAPSHOT

mysql

mysql-connector-java

tk.mybatis

mapper-spring-boot-starter

2.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-common

1.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 this method to configure the {@link HttpSecurity}. Typically subclasses

* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:

*

*

* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

*

*

* @param http the {@link HttpSecurity} to modify

* @throws Exception if an error occurs

*/

@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方法

/**

* Default behaviour for successful authentication.认证成功之后的默认操作

*

*

* {@link SecurityContextHolder}

*

*

* ApplicationEventPublisher

*

*

*

* Subclasses can override this method to continue the {@link FilterChain} after

* successful authentication.

* @param request

* @param response

* @param chain

* @param authResult the object returned from the attemptAuthentication

* method.

* @throws IOException

* @throws ServletException

*/

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方法

/**

* Default behaviour for unsuccessful authentication.认证失败之后的默认操作

*

*

*

* allowSesssionCreation is set to true)

*

*

*

*/

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 {

/**

* 这个方法必须有

* 在过滤器创建的时候手动将AuthenticationManager对象给这个过滤器使用

* @param authenticationManager 这个对象在自己写的SecurityConfig里面

*/

public AuthenticationFilter(AuthenticationManager authenticationManager) {

super.setAuthenticationManager(authenticationManager);

}

/**

* Default behaviour for successful authentication.认证成功之后的默认操作

* @param request

* @param response

* @param chain

* @param authResult the object returned from the attemptAuthentication

* method.

* @throws IOException

* @throws ServletException

*/

@Override

protected void successfulAuthentication(HttpServlehttp://tRequest 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);

}

/**

* Default behaviour for unsuccessful authentication.

* @param request

* @param response

* @param failed

*/

@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);

这个方法干了什么事呢

/**

* 创建令牌

* @param claims

* @return

*/

public static String createJwt(Map claims){

//获取私钥

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 {

/**

* 读取秘钥

* @param keyName

* @return

*/

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 {

/**

* 将结果以json格式返回

* @param result 返回结果

* @param response

* @throws IOException

*/

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 {

/**

* Creates an instance which will authenticate against the supplied

* {@code AuthenticationManager} and which will ignore failed authentication attempts,

* allowing the request to proceed down the filter chain.

* 在过滤器创建的时候手动将AuthenticationManager对象给这个过滤器使用

* @param authenticationManager 这个对象在自己写的SecurityConfig里面

*/

public TokenVerifyFilter(AuthenticationManager authenticationManager) {

super(authenticationManager);

}

/**

* 过滤请求,判断是否携带令牌

* @param request

* @param response

* @param chain

* @throws IOException

* @throws ServletException

*/

@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 验证解析令牌

/**

* 解析令牌

* @param compactJws

* @return

*/

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("解析令牌失败");

}

}

/**

* 解析令牌并获取用户名和权限

* @param compactJws

* @return String[0]用户名

* String[1]权限

*/

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;

/**

* 只有这个配置类有AuthenticationManager对象,我们要把这个类中的这个对象放入容器中

* 这样在别的地方就可以自动注入了

* @return

* @throws Exception

*/

@Bean

@Override

public AuthenticationManager authenticationManager() throws Exception {

AuthenticationManager authenticationManager = super.authenticationManagerBean();

return authenticationManager;

}

/**

* Used by the default implementation of {@link #authenticationManager()} to attempt

* to obtain an {@link AuthenticationManager}. If overridden, the

* {@link AuthenticationManagerBuilder} should be used to specify the

* {@link AuthenticationManager}.

*

*

* The {@link #authenticationManagerBean()} method can be used to expose the resulting

* {@link AuthenticationManager} as a Bean. The {@link #userDetailsServiceBean()} can

* be used to expose the last populated {@link UserDetailsService} that is created

* with the {@link AuthenticationManagerBuilder} as a Bean. The

* {@link UserDetailsService} will also automatically be populated on

* {@link HttpSecurity#getSharedObject(Class)} for use with other

* {@link SecurityContextConfigurer} (i.e. RememberMeConfigurer )

*

*

*

* For example, the following configuration could be used to register in memory

* authentication that exposes an in memory {@link UserDetailsService}:

*

*

*

* @Override

* protected void configure(AuthenticationManagerBuilder auth) {

* auth

* // enable in memory based authentication with a user named

* // "user" and "admin"

* .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()

* .withUser("admin").password("password").roles("USER", "ADMIN");

* }

*

* // Expose the UserDetailsService as a Bean

* @Bean

* @Override

* public UserDetailsService userDetailsServiceBean() throws Exception {

* return super.userDetailsServiceBean();

* }

*

*

*

* @param auth the {@link AuthenticationManagerBuilder} to use

* @throws Exception

*/

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

//在内存中注册一个账号

//auth.inMemoryAuthentication().withUser("user").password("{noop}123").roles("USER");

//连接数据库,使用数据库中的账号

auth.userDetailsService(iUserService).passwordEncoder(bCryptPasswordEncoder);

}

/**

* Override this method to configure {@link WebSecurity}. For example, if you wish to

* ignore certain requests.

*

* @param web

*/

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/css/**",

"/img/**",

"/plugins/**",

"/favicon.ico",

"/loginPage");

}

/**

* Override this method to configure the {@link HttpSecurity}. Typically subclasses

* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:

*

*

* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

*

*

* @param http the {@link HttpSecurity} to modify

* @throws Exception if an error occurs

*/

@Override

protected void configure(HttpSecurity http) throws Exception {

http.csrf().disable()

.httpBasic()

.and()

.authorizeRequests()

.anyRequest().authenticated()

.and()

/**

* 不要将自定义过滤器加component注解,而是在这里直接创建一个过滤器对象加入到过滤器链中,并传入authenticationManager

* 启动后,过滤器链中会同时出现自定义过滤器和他的父类,他会自动覆盖,并不会过滤两次

*

* 使用component注解会产生很多问题:

* 1. web.ignoring()会失效,上面的资源还是会经过自定义的过滤器

* 2.过滤器链中出现的是他们父类中的名字

* 3.登录的时候(访问/login),一直使用匿名访问,不会去数据库中查询

*/

.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 this method to configure the {@link HttpSecurity}. Typically subclasses

* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:

*

*

* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

*

*

* @param http the {@link HttpSecurity} to modify

* @throws Exception if an error occurs

*/

@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);

}

}


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:浅析SpringCloud Alibaba
下一篇:springcloud LogBack日志使用详解
相关文章

 发表评论

暂时没有评论,来抢沙发吧~