整合步骤
- 第一步,给需要登录认证的模块添加mall-security 依赖:
<dependency>
<groupId>com.macro.mall</groupId>
<artifactId>mall-security</artifactId>
</dependency>
- 第二步,添加MallSecurityConfig配置类,继承mall-security中的SecurityConfig配置,并且配置一个UserDetailsService接口的实现类,用于获取登录用户详情:
@Configuration
@EnableWebSecurity
public class yebSecurityConfig extends SecurityConfig {
@Autowired
private IAdminService adminService;
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username-> adminService.getAdminByName(username);
}
}
- 第三步,在application.yml中配置下不需要安全保护的资源路径:
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
secure:
ignored:
urls: #安全路径白名单
- /doc.html
- /swagger-resources/**
- /swagger/**
- /**/v2/api-docs
- /**/*.js
- /**/*.css
- /**/*.png
- /**/*.ico
- /webjars/springfox-swagger-ui/**
- /druid/**
- /actuator/**
- /sso/**
- /login
- 第四步,在UmsMemberController中实现登录和刷新token的接口:
@RestController
public class LoginController {
@Autowired
private IAdminService adminService;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public CommonResult login(AdminLoginParam adminLoginParam){
String token = adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword());
HashMap map = new HashMap<String,String>();
map.put("tokenHead",tokenHead);
map.put("token",token);
return CommonResult.success(map);
}
@GetMapping("/refresh")
public CommonResult refresh(HttpServletRequest request){
String token = request.getHeader(tokenHeader);
String refreshHeadToken = jwtTokenUtil.refreshHeadToken(token);
if (refreshHeadToken == null)
return CommonResult.failed("token已经过期");
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", refreshHeadToken);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
//测试未登录是否被拦截
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
login方法
@Override
public String login(String username, String password) {
String token = null;
HashMap<String, String> map = new HashMap<>();
//密码需要客户端加密后传递
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
System.out.println("密码不正确");
//return "密码不正确";
}
if (!userDetails.isEnabled()) {
System.out.println("帐号已被禁用");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
map.put("Head", "Bearer");
map.put("HeadToken", token);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}
实现原理
将SpringSecurity+JWT的代码封装成通用模块后,就可以方便其他需要登录认证的模块来使用,下面我们来看看它是如何实现的,首先我们看下mall-security的目录结构。
目录结构
mall-security
├── component
| ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
| ├── RestAuthenticationEntryPoint -- 自定义返回结果:未登录或登录过期
| └── RestfulAccessDeniedHandler -- 自定义返回结果:没有权限访问时
├── config
| ├── IgnoreUrlsConfig -- 用于配置不需要安全保护的资源路径
| └── SecurityConfig -- SpringSecurity通用配置
└── util
└── JwtTokenUtil -- JWT的token处理工具类
做了哪些变化
其实我也就添加了两个类,一个IgnoreUrlsConfig,用于从application.yml中获取不需要安全保护的资源路径。一个SecurityConfig提取了一些SpringSecurity的通用配置。
- IgnoreUrlsConfig中的代码:
/**
* 用于配置不需要保护的资源路径
* Created by macro on 2018/11/5.
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List<String> urls = new ArrayList<>();
}
- SecurityConfig中的代码:
/**
* 对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑
* Created by macro on 2019/11/5.
*/
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
//允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
return new IgnoreUrlsConfig();
}
@Bean
public JwtTokenUtil jwtTokenUtil() {
return new JwtTokenUtil();
}
}