前言
springsecurity作為和shiro并駕齊驅的安全框架,我從工作中發現他們其實功能都是差不多的,只不過springsecurity難度更加大一點,很多接口和類都需要查文檔才能梳理出來。不過springsecurity有著spring完美的加持,加之現在微服務大行其道,能在springsecurity的基礎上構建基于SpringAuth2.0的分布式安全管理,所以新的項目使用springsecurity更加好。springsecurity也和他官網所說的那樣非常spring-非常容易擴展。
主要類的功能概述
-
AuthenticationToken
: 所有請求都會封裝成AuthenticationToken,再交給AuthenticationManager
去驗證,核心實現就是UsernamePasswordAuthenticationToken
. -
AuthenticationManager
:這個接口是所有認證管理的中心,所有的請求都會將請求信息封裝為Authentication的實現類,再經過它的authenticate(Authentication authentication)
方法進行認證或者授權,返回一個經過認證或者授權的Authentication對象.他有許多實現,最重要一個核心實現就是ProviderManager
(圖1),最后調用這個實現類的authenticate()
方法(圖2).這個方法的主要內容是調用AuthenticationProvider
進行驗證和授權.
-
AuthenticationProvider
:AuthenticationManager的authenticate方法最終調用的就是AuthenticationProvider
的authenticate()
的方法,當然這個也是非常的spring(為你提供了各種各樣的實現),我們最重要的當然是基于數據庫(圖3)的驗證方式,也就是DaoAuthenticationProvider
,這也是默認的驗證方式.
-
UserDetailsService
: 這個接口主要定義loadUserByUsername(String name)
方法,也就是根據用戶名從數據庫中查詢用戶,所以需要用戶提供自己的實現.AuthenticationProvider
驗證的核心原理就是:從UserDetailsService中查詢數據庫中用戶的密碼,再和用戶登錄的密碼比較,如果匹配就說明驗證成功,也就是additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
方法(圖4).
圖4 AuthenticationSuccessHandler
和AuthenticationFailureHandler
:驗證/認證成功和失敗都是通過Handler來處理(圖5、圖5-1),這個比較簡單.一般我們在驗證成功以后生成token,認證成功以后返回成功標識即可。在AbstractAuthenticationProcessingFilter
中處理失敗和成功。
-
SecurityContext
:當所有的驗證成功以后,返回一個Authentication,這個就是用戶的回話上下文,我們需要一個容器把他保存起來,這個時候就是SecurityContext
來做,SecurityHolder.getSecruityContext()就可以得到用戶信息.
主要流程概述
springsecurity主要有兩個功能:
- 驗證:即Authenrization,主要解決"你是誰",也就是登錄的時候驗證你的合法性、記錄用戶的權限信息,驗證通過后返回token.
- 認證:即Authentication,主要解決"你能做什么",用戶拿著token去請求除了登錄退出之外的其他資源是否有相應的權限控制.
所以基本上本demo就是圍繞這兩個核心的流程展開的
新建jwt工具類,加密解密jwt,此處略
新建用戶、角色、權限表,以及中間表
注意:用戶表要實現UserDetails
,角色表要實現GrantedAuthority
- 用戶表
@Data
@Entity
@Table(name = "user")
@ToString
public class MyUser extends BaseEntity implements UserDetails {
/**
* @JoinTable:name-中間表的名字
* JoinColumn:當前表的referencedColumnName的字段(id)在中間表的字段名字(user_id)
* inverseJoinColumns: 關聯外鍵表的referencedColumnName的字段(id)在中間表的字段名字(role_id)
*/
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> roles;
@Column(unique = true, length = 32, columnDefinition = "varchar(32) DEFAULT '' COMMENT '用戶名'")
private String username;
@Column(length = 50, columnDefinition = " varchar(50) DEFAULT '' COMMENT '密碼'")
private String password;
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 獲取權限列表
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 角色表
@Data
@Entity
public class Role extends BaseEntity implements GrantedAuthority{
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '角色名稱/菜單名'")
private String name;
@Override
public String getAuthority() {
return name;
}
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "role_permission", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "permit_id", referencedColumnName = "id"))
private List<Permission> permissions;
@Column(length = 1024, columnDefinition = "varchar(1024) default '' COMMENT '內容'")
private String descpt;
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '角色編號'")
private String code;
@Column(length = 10, columnDefinition = "int(10) COMMENT '插入者id'")
private Integer insertUid;
}
- 權限表
@Data
@Entity
public class Permission extends BaseEntity{
@Column(columnDefinition = "varchar(64) default '' COMMENT '權限名稱'")
private String name;
/*@ManyToMany(mappedBy = "permissions")
private List<Role> roles;*/
@Column(length = 10, columnDefinition = "int(10) COMMENT '父菜單id'")
private Integer pid;
@Column(length = 10, columnDefinition = "int(10) COMMENT '菜單排序'")
private Integer zindex;
@Column(length = 1, columnDefinition = "int(1) COMMENT '權限分類(0 菜單;1 功能)'")
private Integer istype;
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '權限描述'")
private String descpt;
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '圖標'")
private String icon;
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '代號'")
private String code;
@Column(length = 64, columnDefinition = "varchar(64) default '' COMMENT '菜單url'")
private String page;
}
實現UserDetailsService
@Service
public class JwtUserService implements UserDetailsService {
private static Logger LOGGER = LoggerFactory.getLogger(JwtUserService.class);
@Autowired
UserDao userDao;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 從數據庫中查詢用戶,密碼應該是數據庫加密的密碼,但是這里和登錄的時候一致,使用寫死的密碼
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser user = new UserDao().findUserByUsername(username);
String password = passwordEncoder.encode("123456");
user.setPassword(password);
LOGGER.info("查詢到用戶信息:{}",user.toString());
return user;
}
/**
* 用戶注冊
* @param user
*/
public void regisUser(MyUser user){
}
public void deleteUserJwt(){
}
驗證
新建JwtAuthenticationFilter類繼承UsernamePasswordAuthenticationFilter
,這個過濾器攔截"/login"路徑(其實這個是默認的,寫出來方便看而已),攔截后生成AuthenticationToken
交給AuthenticationManager
去驗證,驗證成功就生成jwt返回給發起者.
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private AuthenticationManager authenticationManager;
/**
* 在構造器中設置攔截的路勁,默認攔截的是"/login"
* 在構造器中設置AuthenticationManager
*/
public JwtAuthenticationFilter(AuthenticationManager authenticationManager){
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
this.authenticationManager=authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//從json中獲取username和password
UsernamePasswordAuthenticationToken token = null;
try {
String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
String username = null, password = null;
if (StringUtils.hasText(body)) {
JSONObject jsonObj = JSONObject.parseObject(body);
username = jsonObj.getString("username");
password = jsonObj.getString("password");
}
if (username == null){
username = "";
}
if (password == null){
password = "";
}
username = username.trim();
token = new UsernamePasswordAuthenticationToken(username,password);
LOGGER.info("get user info from login success,name:{}",token.getName());
} catch (IOException e) {
LOGGER.error("get user info from login failed,reason:{}",e.getMessage());
}
//封裝后的token最終是交給provider來處理
Authentication authenticate = authenticationManager.authenticate(token);
return authenticate;
}
/**
* 驗證成功之后的回調,可以自己實現AuthenticationSuccessHandler處理(JwtLoginSuccessHandler)
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
MyUser user= (MyUser) authResult.getPrincipal();
String jwt = JwtTokenUtils.createToken(user);
StringBuffer buffer = new StringBuffer(CommonConst.TOKEN_PREFIX);
buffer.append(jwt);
LOGGER.info("authentication success,user:【{}】,jwt:【{}】",user.toString(),buffer.toString());
response.setHeader(CommonConst.JWTHEADER, buffer.toString());
}
/**
* 驗證失敗之后的回調,可以自己實現AuthenticationFailureHandler處理(JwtLoginFailureHandler)
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String message = failed.getCause().getMessage();
LOGGER.error("authentication failed, reason:{}",message);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}
認證
新建JwtAuthenrizationFilter繼承BasicAuthenticationFilter
,取出用戶的jwt,解密jwt
public class JwtAuthenrizationFilter extends BasicAuthenticationFilter {
Logger LOGGER = LoggerFactory.getLogger(JwtAuthenrizationFilter.class);
@Autowired
JwtUserService userService;
public JwtAuthenrizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* @param tokenHeader
* @return
*/
protected UsernamePasswordAuthenticationToken getToken(String tokenHeader) {
LOGGER.info("Authenrization jwt:{}",tokenHeader);
String token = tokenHeader.replace(CommonConst.TOKEN_PREFIX, "");
String name = JwtTokenUtils.getUserNameByToken(token);
LOGGER.info("Authenrization username:{}",name);
UserDetails userDetails = userService.loadUserByUsername(name);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
UsernamePasswordAuthenticationToken passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(name, null, userDetails.getAuthorities());
return passwordAuthenticationToken;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(CommonConst.JWTHEADER);
// 如果請求頭中沒有Authorization信息則直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(CommonConst.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
//有jwt則需要驗證
UsernamePasswordAuthenticationToken authenticationToken = getToken(tokenHeader);
//剩下的就交給authenticationManager、provider去做
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
super.doFilterInternal(request, response, chain);
}
}
統一配置
以上是主要類的建立,下面我們需要加載這些配置,使得他們可以生效.新建SecurityConfig繼承WebSecurityConfigurerAdapter
- 配置攔截路勁,也就是antMatchers()方法,默認的登錄“/login”和退出"/logout"是不需要配置的;另外可以配置路徑具有哪些權限
- 跨域配置
- 加入驗證和認證的filter,也就是
JwtAuthenticationFilter
和JwtAuthenrizationFilter
@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)開啟方法級別的安全注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("jwtUserService")
private UserDetailsService userDetailsService;
/**
* 注入加密
* @return
*/
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在這里指定密碼的加密方式,SpringSecutity5.0之后必須指定
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
//auth.authenticationProvider(authenticationProvider());
/* auth.inMemoryAuthentication() //認證信息存儲到內存中
.passwordEncoder(passwordEncoder())
.withUser("zhouyu").password(passwordEncoder().encode("123456")).roles("ADMIN");*/
}
/**
* 默認使用的就是DaoAuthenticationProvider,在這里只是顯示的寫出來參考
* @param http
* @throws Exception
*/
/* @Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/image/**").permitAll()
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.antMatchers("/article/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.csrf().disable()
//.formLogin().disable()
//不需要session
.sessionManagement().disable()
//跨域允許
.cors()
.and()
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
new Header("Access-control-Allow-Origin","*"),
new Header("Access-Control-Expose-Headers","Authorization"))))
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthenrizationFilter(authenticationManager()))
.logout()
.addLogoutHandler(new JwtLogoutHandler())
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.and()
.sessionManagement().disable();
}
/**
* 跨域配置
* @return
*/
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
postman模擬測試
使用postman發出login請求,后臺會返回一個jwt,我們在拿著jwt去訪問首頁index,驗證通過會有日志顯示
現在已經完成了驗證和授權的全部,細心的你可能發現了,現實的權限管理是動態的:用戶訪問一個url,我們需要根據用戶的權限來判斷用戶是否具有訪問的權限.我們將在下一篇中介紹動態的權限管理如何實現.