一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是Spring Security重要核心功能。
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro
Spring Boot/Spring Cloud + Spring Security
主体
英文单词:principal
使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。
认证
英文单词:authentication
权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。
笼统的认为就是以前所做的登录操作。
授权
英文单词:authorization
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
所以简单来说,授权就是给用户分配权限。
完成权限管理需要三个对象
用户:主要包含用户名、密码和当前用户的角色信息,可实现认证操作。
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,二者是通过角色来建立关联关系的。
Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。
1、Spring Security主要jar包功能介绍
spring-security-core.jar
核心包,任何Spring Security功能都需要此包。
spring-security-web.jar
web工程必备,包含过滤器和相关的Web安全基础结构代码。
spring-security-config.jar
用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
spring-security-taglibs.jar
Spring Security提供的动态标签库,jsp页面可以用。
2、配置web.xml
<filter>
<filter-name>springSecurityFilterChainfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class> filter>
<filter-mapping>
<filter-name>springSecurityFilterChainfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
3、spring-security.xml
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/> security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user" authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
security:user-service>
security:authentication-provider>
security:authentication-manager>
1. context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。
2. context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3. header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4. csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。
5. authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
6. authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7. authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8. authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
9. authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10. savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11. servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12. authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13. session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
14. access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15. access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
1、DelegatingFilterProxy
我们在web.xml中配置了一个名称为springSecurityFilterChain的过滤器DelegatingFilterProxy,接下我直接对DelegatingFilterProxy源码里重要代码进行说明,其中删减掉了一些不重要的代码,大家注意我写的注释就行了!
public class DelegatingFilterProxy extends GenericFilterBean {
@Nullable
private String contextAttribute;
@Nullable
private WebApplicationContext webApplicationContext;
@Nullable
private String targetBeanName;
private boolean targetFilterLifecycle;
@Nullable
private volatile Filter delegate;
//注:这个过滤器才是真正加载的过滤器
private final Object delegateMonitor;
//注:doFilter才是过滤器的入口,直接从这看!
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain filterChain
)
throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException(
"No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?"
);
}
//第一步:doFilter中最重要的一步,初始化上面私有过滤器属性
delegate delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
//第三步:执行FilterChainProxy过滤器
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
//第二步:直接看最终加载的过滤器到底是谁
protected Filter initDelegate(WebApplicationContext wac)
throws ServletException {
//debug得知targetBeanName为:springSecurityFilterChain
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
//debug得知delegate对象为:FilterChainProxy
Filter delegate = (Filter) wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
protected void invokeDelegate(
Filter delegate,
ServletRequest request,
ServletResponse response,
FilterChain filterChain
)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
}
第二步debug结果如下:
由此可知,DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器,最终在第三步执行了这个过滤器。
2、 FilterChainProxy
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
private static final String FILTER_APPLIED =
FilterChainProxy.class.getName().concat(".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainProxy.FilterChainValidator filterChainValidator;
private HttpFirewall firewall;
//咿!?可以通过一个叫SecurityFilterChain的对象实例化出一个FilterChainProxy对象
//这FilterChainProxy又是何方神圣?会不会是真正的过滤器链对象呢?先留着这个疑问!
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
//又是SecurityFilterChain这家伙!嫌疑更大了!
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
this.firewall = new StrictHttpFirewall();
this.filterChains = filterChains;
}
//注:直接从doFilter看
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
this.doFilterInternal(request, response, chain);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} else {
//第一步:具体操作调用下面的doFilterInternal方法了
this.doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(
ServletRequest request,
ServletResponse response,
FilterChain chain
)
throws IOException, ServletException {
FirewalledRequest fwRequest =
this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse =
this.firewall.getFirewalledResponse((HttpServletResponse) response);
//第二步:封装要执行的过滤器链,那么多过滤器就在这里被封装进去了!
List<Filter> filters = this.getFilters((HttpServletRequest) fwRequest);
if (filters != null && filters.size() != 0) {
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(
fwRequest,
chain,
filters
);
//第四步:加载过滤器链
vfc.doFilter(fwRequest, fwResponse);
} else {
if (logger.isDebugEnabled()) {
logger.debug(
UrlUtils.buildRequestUrl(fwRequest) +
(
filters == null
? " has no matching filters"
: " has an empty filter list"
)
);
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
}
}
private List<Filter> getFilters(HttpServletRequest request) {
Iterator var2 = this.filterChains.iterator();
//第三步:封装过滤器链到SecurityFilterChain中!
SecurityFilterChain chain;
do {
if (!var2.hasNext()) {
return null;
}
chain = (SecurityFilterChain) var2.next();
} while (!chain.matches(request));
return chain.getFilters();
}
}
第二步debug结果如下图所示,惊不惊喜?十五个过滤器都在这里了!
再看第三步,怀疑这么久!原来这些过滤器还真是都被封装进SecurityFilterChain中了。
3、SecurityFilterChain
最后看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!
//接口
public interface SecurityFilterChain {
boolean matches(HttpServletRequest var1);
List<Filter> getFilters();
}
//实现类
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(
DefaultSecurityFilterChain.class
);
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public DefaultSecurityFilterChain(
RequestMatcher requestMatcher,
Filter... filters
) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(
RequestMatcher requestMatcher,
List<Filter> filters
) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
this.requestMatcher = requestMatcher;
this.filters = new ArrayList(filters);
}
public RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
public List<Filter> getFilters() {
return this.filters;
}
public boolean matches(HttpServletRequest request) {
return this.requestMatcher.matches(request);
}
public String toString() {
return "[ " + this.requestMatcher + ", " + this.filters + "]";
}
}
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" authentication-failure-url="/failer.jsp"/>
<security:logout logout-url="/logout" logout-success-url="/login.jsp"/>
security:http>
CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
1、CsrfFilter过滤器说明
通过源码分析,我们明白了,自己的认证页面,请求方式为POST,但却没有携带token,所以才出现了403权限不足的异常。那么如何处理这个问题呢?
方式一:直接禁用csrf,不推荐。
方式二:在认证页面携带token请求。
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new
CsrfFilter.DefaultRequiresCsrfMatcher();
private final Log logger = LogFactory.getLog(this.getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
//通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//第一类:"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过,这些请求都不会改变数据
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
//第二类:除去上面四类,包括POST都要被验证携带token才能通过
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new
InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}
2、禁用csrf防护机制
<security:csrf disabled="true"/>
3、在认证页面携带token请求
1、UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter
extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
)
throws AuthenticationException {
//必须为POST请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod()
);
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//将填写的用户名和密码封装到了UsernamePasswordAuthenticationToken中
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username,
password
);
this.setDetails(request, authRequest);
//调用AuthenticationManager对象实现认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
2、AuthenticationManager
由上面源码得知,真正认证操作在AuthenticationManager里面!
然后看AuthenticationManager的实现类ProviderManager:
public class ProviderManager
implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher;
private List<AuthenticationProvider> providers;
protected MessageSourceAccessor messages;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication;
//注意AuthenticationProvider这个对象,SpringSecurity针对每一种认证,什么qq登录啊,
//用户名密码登陆啊,微信登录啊都封装了一个AuthenticationProvider对象。
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager) null);
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
//循环所有AuthenticationProvider,匹配当前认证类型。
while (var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider) var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug(
"Authentication attempt using " + provider.getClass().getName()
);
}
try {
//找到了对应认证类型就继续调用AuthenticationProvider对象完成认证业务。
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {} catch (
AuthenticationException var12
) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (
this.eraseCredentialsAfterAuthentication &&
result instanceof CredentialsContainer
) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException =
new ProviderNotFoundException(
this.messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"
)
);
}
if (parentException == null) {
this.prepareException(
(AuthenticationException) lastException,
authentication
);
}
throw lastException;
}
}
}
3、AbstractUserDetailsAuthenticationProvider
咱们继续再找到AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider:
public class DaoAuthenticationProvider
extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
protected final UserDetails retrieveUser(
String username,
UsernamePasswordAuthenticationToken authentication
)
throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//重点来了!主要就在这里了!
//可别忘了,咱们为什么要翻源码,是想用自己数据库中的数据实现认证操作啊!
//UserDetails就是SpringSecurity自己的用户对象。
//this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类
//loadUserByUsername里面就是真正的认证逻辑
//也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了!
//loadUserByUsername方法中只需要返回一个UserDetails对象即可
UserDetails loadedUser =
this.getUserDetailsService().loadUserByUsername(username);
//若返回null,就抛出异常,认证失败。
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation"
);
} else {
//若有得到了UserDetails对象,返回即可。
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
}
4、AbstractUserDetailsAuthenticationProvider中authenticate返回值
按理说到此已经知道自定义认证方法的怎么写了,但咱们把返回的流程也大概走一遍,上面不是说到返回了一个UserDetails对象对象吗?跟着它,就又回到了AbstractUserDetailsAuthenticationProvider对象中authenticate方法的最后一行了。
5、UsernamePasswordAuthenticationToken
来到UsernamePasswordAuthenticationToken对象发现里面有两个构造方法
6、AbstractAuthenticationToken
7AbstractAuthenticationProcessingFilter
可见AbstractAuthenticationProcessingFilter这个过滤器对于认证成功与否,做了两个分支,成功执行successfulAuthentication,失败执行unsuccessfulAuthentication。 在successfulAuthentication内部,将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是常见的“记住我”功能!此功能具体应用,咱们后续再研究!
1、让我们自己的UserService接口继承UserDetailsService
public interface UserService extends UserDetailsService {
public void save(SysUser user);
public List<SysUs er> findAll();
public Map<String, Object> toAddRolePage(Integer id);
public void addRoleToUser(Integer userId, Integer[] ids);
}
2、编写loadUserByUsername认证业务
/**
* UserDetail是SpringSecurity自己的用户对象
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if(sysUser==null){
//若用户名不对,直接返回null,表示认证失败。
return null;
}
//用户要已经拥有一些角色
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
//最终需要返回一个SpringSecurity的UserDetails对象,而UserDetail是一个接口,可以new一个它的实现类即User,{noop}表示不加密认证。
//authorities是认证的所有角色的集合
//{noop}后面的密码,SpringSecurity会认为是原文
return new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
}
3、在SpringSecurity主配置文件中指定认证使用的业务对象
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
security:authentication-provider>
security:authentication-manager>
1、在IOC容器中提供加密对象
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="passwordEncoder"/>
security:authentication-provider>
security:authentication-manager>
2、修改认证方法
去掉{noop}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if(sysUser==null){
//若用户名不对,直接返回null,表示认证失败。
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
//最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
3、修改添加用户的操作
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void save(SysUser user) {
//对密码进行加密,手动把数据库中的密码设置成密文,然后再入库
user.setPassword(passwordEncoder.encode(user.getPassword()));
userDao.save(user);
}
//……
}
1、源码分析
用户认证业务里,我们封装User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority>
authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
可以看到,这个构造方法里多了四个布尔类型的构造参数,其实我们使用的三个构造参数的构造方法里这四个布尔值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?
boolean enabled 是否可用
boolean accountNonExpired 账户是否失效
boolean credentialsNonExpired 秘密是否失效
boolean accountNonLocked 账户是否锁定
2、判断认证用户的状态
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if(sysUser==null){
//若用户名不对,直接返回null,表示认证失败。
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
//最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
return new User(sysUser.getUsername(),
sysUser.getPassword(),
sysUser.getStatus()==1, // 此刻,只有用户状态为1的用户才能成功通过认证!
true,
true,
true, authorities);
}
1、记住我功能原理分析
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
// 判断是否勾选记住我
// 注意:这里this.parameter点进去是上面的private String parameter = "remember-me";
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
//若勾选就调用onLoginSuccess方法
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
}
再点进去上面if判断中的rememberMeRequested方法,还在当前类中:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
// 从上面的字parameter的值为"remember-me"
// 也就是说,此功能提交的属性名必须为"remember-me"
String paramValue = request.getParameter(parameter);
// 这里我们看到属性值可以为:true,on,yes,1。
if (paramValue != null && (paramValue.equalsIgnoreCase("true") ||
paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") ||
paramValue.equals("1"))) {
//满足上面条件才能返回true
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set
parameter '" + parameter + "')");
}
return false;
}
}
}
如果上面方法返回true,就表示页面勾选了记住我选项了。
继续顺着调用的方法找到PersistentTokenBasedRememberMeServices的onLoginSuccess方法:
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// 获取用户名
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
//创建记住我的token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username,
this.generateSeriesData(), this.generateTokenData(), new Date());
try {
//将token持久化到数据库
this.tokenRepository.createNewToken(persistentToken);
//将token写入到浏览器的Cookie中
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
}
2、开启remember me过滤器
<security:http auto-config="true" use-expressions="true">
<security:remember-me token-validity-seconds="60"/>
security:http>
3、持久化remember me信息
创建一张表,注意这张表的名称和字段都是固定的,不要修改
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
然后将spring-security.xml中 改为:
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="60"
remember-me-parameter="remember-me"/>
1、为了模拟授权操作,咱们临时编写两个业务功能:
//ProductController
@Controller
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
//OrderController
@Controller
@RequestMapping("/order")
public class OrderController {
@RequestMapping("/findAll")
public String findAll(){
return "order-list";
}
}
<ul class="treeview-menu">
<li id="system-setting"><a href="${pageContext.request.contextPath}/product/findAll">
<i class="fa fa-circle-o">i> 产品管理a>
li>
<li id="system-setting"><a href="${pageContext.request.contextPath}/order/findAll">
<i class="fa fa-circle-o">i> 订单管理a>
li>
ul>
2、授权操作
说明:SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在controller类中,对应注解支持应该放在mvc配置文件中,因为controller类是有mvc配置文件扫描并创建的,同理,注解放在service类中,对应注解支持应该放在spring配置文件中。由于我们现在是模拟业务操作,并没有service业务代码,所以就把注解放在controller类中了。
3、开启授权的注解支持
<security:global-method-security jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>
4、在注解支持对应类或者方法上添加注解
//表示当前类中所有方法都需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
@RolesAllowed({"ROLE_ADMIN","ROLE_PRODUCT"})//JSR-250注解
public class ProductController {
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
//表示当前类中findAll方法需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/findAll")
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')")//spring表达式注解
public String findAll(){
return "product-list";
}
}
//表示当前类中所有方法都需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})//SpringSecurity注解
public class ProductController {
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用JWT来实现token的生成和校验。
JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可以解析检验token。
JWT生成的token由三部分组成:
头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
优点:安全,难以破解
缺点:算法比较耗时,为了安全,可以接受
历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA。
1、JWT相关工具类
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.7version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
2、载荷对象
/**
* @author 黑马程序员
* 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象
*/
@Data
public class Payload<T> {
private String id;
private T userInfo;
private Date expiration;
}
3、JWT工具类
/**
* @author: 黑马程序员
* 生成token以及校验token相关方法
*/
public class JwtUtils {
private static final String JWT_PAYLOAD_USER_KEY = "user";
/**
* 私钥加密token
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey,
int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey,
int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
private static String createJTI() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T>
userType) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(),
userType));
claims.setExpiration(body.getExpiration());
return claims;
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
}
4、RSA工具类
/**
* @author 黑马程序员
*/
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException,
InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String
secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
回顾集中式认证流程
用户认证:
使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤器父类中successfulAuthentication方法实现认证成功后的操作。
身份校验:
使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器
分析分布式认证流程
用户认证:
由于,分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。
另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
身份校验:
原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。
1、通用模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLrPP17B-1648283430507)(https://gitee.com/qingxi5/typora/raw/master/note/20210629152137.png)]
在通用子模块中编写测试类生成rsa公钥和私钥
public class RsaUtilsTest {
private String publicFile = "D:\\auth_key\\rsa_key.pub";
private String privateFile = "D:\\auth_key\\rsa_key";
@Test
public void generateKey() throws Exception {
RsaUtils.generateKey(publicFile, privateFile, "heima", 2048);
}
}
2、认证服务
2.1 创建认证服务配置文件
server:
port: 9001
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
heima:
key:
pubKeyPath: D:\\auth_key\\rsa_key.pub
priKeyPath: D:\\auth_key\\rsa_key
2.2 提供解析公钥和私钥的配置类
@Data
@ConfigurationProperties(prefix = "heima.key")
public class RsaKeyProperties {
private String pubKeyPath;
private String priKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
@PostConstruct
public void loadKey() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyPath);
privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
}
2.3、创建认证服务启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
2.4、编写认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop)
{
this.authenticationManager = authenticationManager;
this.prop = prop;
}
/**
* 接收并解析用户凭证,出現错误时,返回json数据前端
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
{
try {
//将json格式请求体转成JavaBean对象
SysUser user = new ObjectMapper().readValue(req.getInputStream(), SysUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword())
);
} catch (Exception e) {
try {
//如果认证失败,提供自定义json格式异常
res.setContentType("application/json;charset=utf-8");
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = res.getWriter();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", "账号或密码错误!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}
/**
* 用户登录成功后,生成token,并且返回json数据给前端
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth) {
//得到当前认证的用户对象
SysUser user = new SysUser();
user.setUsername(auth.getName());
user.setRoles((List<SysRole>) auth.getAuthorities());
//json web token构建
String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24*60);
//返回token
res.addHeader("Authorization", "Bearer " + token);
try {
//登录成功時,返回json格式进行提示
res.setContentType("application/json;charset=utf-8");
res.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = res.getWriter();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", HttpServletResponse.SC_OK);
map.put("message", "登陆成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
2.5 编写检验token过滤器
public class TokenVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop)
{
super(authenticationManager);
this.prop = prop;
}
/**
* 过滤请求
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) {
try {
//请求体的头中是否包含Authorization
String header = request.getHeader("Authorization");
//Authorization中是否包含Bearer,不包含直接返回
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
responseJson(response);
return;
}
//获取权限失败,会抛出异常
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
//获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
responseJson(response);
e.printStackTrace();
}
}
/**
* 未登录提示
* @param response
*/
private void responseJson(HttpServletResponse response) {
try {
//未登录提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "请登录!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
/**
* 通过token,获取用户信息
*
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
//通过token解析出载荷信息
Payload<SysUser> payload = JwtUtils.getInfoFromToken(token.replace("Bearer ", ""),
prop.getPublicKey(), SysUser.class);
SysUser user = payload.getUserInfo();
//不为null,返回
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, user.getRoles());
}
return null;
}
return null;
}
}
2.6 编写SpringSecurity配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Autowired
private RsaKeyProperties prop;
@Bean
public BCryptPasswordEncoder myPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭跨站请求防护
.cors().and().csrf().disable()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests().antMatchers("/product").hasAnyRole("USER")
//其他的需要授权后访问
.anyRequest().authenticated()
.and()
//增加自定义认证过滤器
.addFilter(new TokenLoginFilter(authenticationManager(), prop))
//增加自定义验证认证过滤器
.addFilter(new TokenVerifyFilter(authenticationManager(), prop))
// 前后端分离是无状态的,不用session了,直接禁用。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//UserDetailsService类
auth.userDetailsService(myCustomUserService)
//加密策略
.passwordEncoder(myPasswordEncoder());
}
}
3、资源服务
资源服务中只能通过公钥验证认证。不能签发token
3.1 编写产品服务配置文件,切记这里只能有公钥地址!
server:
port: 9002
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
heima:
key:
pubKeyPath: D:\\auth_key\\rsa_key.pub
3.2 编写读取公钥的配置类
@Data
@ConfigurationProperties(prefix = "heima.key")
public class RsaKeyProperties {
private String pubKeyPath;
private PublicKey publicKey;
@PostConstruct
public void loadKey() throws Exception {
publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
3.3 编写启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
3.4 复制认证服务中SpringSecurity配置类做修改,去掉“增加自定义认证过滤器”即可!
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Autowired
private RsaKeyProperties prop;
@Bean
public BCryptPasswordEncoder myPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭跨站请求防护
.cors().and().csrf().disable()
//允许不登陆就可以访问的方法,多个用逗号分隔
.authorizeRequests().antMatchers("/product").hasAnyRole("USER")
//其他的需要授权后访问
.anyRequest().authenticated()
.and()
//增加自定义验证认证过滤器
.addFilter(new TokenVerifyFilter(authenticationManager(), prop))
// 前后端分离是无状态的,不用session了,直接禁用。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//UserDetailsService类
auth.userDetailsService(myCustomUserService)
//加密策略
.passwordEncoder(myPasswordEncoder());
}
}
3.5 编写产品处理器
@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping
public String findAll(){
return "产品测试成功!";
}
}
先说OAuth,OAuth是Open Authorization的简写。
OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
OAuth2.0是OAuth协议的延续版本,但不向前兼容(即完全废止了OAuth1.0)。
使用场景
假设,A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者原本毫无关联。
如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。
按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,现在有了OAuth2,只需要A网站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有效,只要满足A网站打印照片需求即可。
这么听来,是不是有点像单点登录?NONONO!千万不要混淆概念!单点登录是用户一次登录,自己可以操作其他关联的服务资源。OAuth2则是用户给一个系统授权,可以直接操作其他系统资源的一种方式。
但SpringSecurity的OAuth2也是可以实现单点登录的!
总结一句:SpringSecurity的OAuth2可以做服务之间资源共享,也可以实现单点登录!
1、授权码模式(authorization code)
流程
说明:【A服务客户端】需要用到【B服务资源服务】中的资源
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备【B服务认证服务】返回授权码使用。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生成授权码,授权码将通过第一步提供的回调地址,返回给【A服务客户端】。注意这个授权码并非通行【B服务资源服务】的通行凭证。
第四步:【A服务认证服务】携带上一步得到的授权码向【B服务认证服务】发送请求,获取通行凭证token。
第五步:【B服务认证服务】给【A服务认证服务】返回令牌token和更新令牌refresh token。
使用场景
授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。
2、简化模式(implicit)
流程
说明:简化模式中没有【A服务认证服务】这一部分,全部有【A服务客户端】与B服务交互,整个过程不再有
授权码,token直接暴露在浏览器。
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备
【B服务认证服务】返回token使用,还会携带一个【A服务客户端】的状态标识state。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也
就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生成通行令牌token,token将通过第一步提供的回调地址,返回给【A服务客户
端】。
使用场景
适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。
3、密码模式(resource owner password credentials)
流程
第一步:直接告诉【A服务客户端】自己的【B服务认证服务】的用户名和密码
第二步:【A服务客户端】携带【B服务认证服务】的用户名和密码向【B服务认证服务】发起请求获取
token。
第三步:【B服务认证服务】给【A服务客户端】颁发token。
使用场景
此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。
4、客户端模式(client credentials)
流程
说明:这种模式其实已经不太属于OAuth2的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取
token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。
第一步:A服务向B服务索取token。
第二步:B服务返回token给A服务。
使用场景
A服务本身需要B服务资源,与用户无关。
1、创建资源模块
提供配置文件
server:
port: 9002
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
main:
allow-bean-definition-overriding: true
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
提供启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
public class OAuthSourceApplication {
public static void main(String[] args) {
SpringApplication.run(OAuthSourceApplication.class, args);
}
}
提供处理器
@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping
public String findAll(){
return "查询产品列表成功!";
}
}
编写资源管理配置类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
/**
* TokenStore是OAuth2保存token的接口
* 其下有RedisTokenStore保存到redis中,
* JdbcTokenStore保存到数据库中,
* InMemoryTokenStore保存到内存中等实现类,
* 这里我们选择保存在数据库中
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources)throws Exception{
TokenStore tokenStore = new JdbcTokenStore(dataSource);
resources.resourceId("product_api")//指定当前资源的id,非常重要!必须写!
.tokenStore(tokenStore);//指定保存token的方式
}
@Override
public void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
//指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
.and()
.headers().addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请
求头信息
response.setHeader("Access-Control-Allow-Methods", request.getHeader("AccessControl-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("AccessControl-Request-Headers"));
}
});
}
}
2、创建授权模块
提供配置文件
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
main:
allow-bean-definition-overriding: true # 这个表示允许我们覆盖OAuth2放在容器中的bean对象,一定要
配置
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
提供启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
public class OauthServerApplication {
public static void main(String[] args) {
SpringApplication.run(OauthServerApplication.class, args);
}
}
提供SpringSecurity配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myCustomUserService;
@Bean
public BCryptPasswordEncoder myPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//所有资源必须授权后访问
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()//指定认证页面可以匿名访问
//关闭跨站请求防护
.and().csrf().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//UserDetailsService类
auth.userDetailsService(myCustomUserService)
//加密策略
.passwordEncoder(myPasswordEncoder());
}
//AuthenticationManager对象在OAuth2认证服务中要使用,提取放入IOC容器中
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
提供OAuth2授权配置类
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
//从数据库中查询出客户端信息
@Bean
public JdbcClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
//token保存策略
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
//授权信息保存策略
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
//授权码模式专用对象
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
//指定客户端登录信息来源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
}