业务流程
集成流程
// 不为空再进行安全上下文的生成和赋予;如果为空直接放行,下一个过滤器会收拾他,不过不要修改加解密bean
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
总而言之就是要:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>team.sssgroupId>
<artifactId>open-platformartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.5.RELEASEversion>
parent>
<properties>
<spring.cloud-version>Hoxton.SR8spring.cloud-version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring.cloud-version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.3.3.RELEASEversion>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.8.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.75version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-coreartifactId>
<version>5.7.9version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.1.0.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.26version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.20version>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>2.8.1version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.3.RELEASEversion>
plugin>
plugins>
build>
project>
这里的jwt指的是用于维持资源所有者登录状态时使用jwt
此配置管理资源的认证,用于配置资源的访问规则
把客户端写在内存
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final AdopApplicationService adopApplicationService;
private final CustomJwtTokenFilter customJwtTokenFilter;
public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, CustomJwtTokenFilter customJwtTokenFilter) {
this.passwordEncoder = passwordEncoder;
this.adopApplicationService = adopApplicationService;
this.customJwtTokenFilter = customJwtTokenFilter;
}
// 配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 加载合作伙伴应用的信息(模板)
// clients.inMemory()
// .withClient("pzh") // clientId,客户端id
// // 客户端密码,客户端传输过来的密钥会进行加密,就用你注入进去的那个,所以如果你是明文就需要在这里进行加密以后写入,如果你数据库存的就是密文,则直接写入
// .secret(passwordEncoder.encode("123456"))
// // 重定向的地址,用户同意授权以后会携带授权码请求回调地址,从而获取授权码
// .redirectUris("http://localhost:9998/oauth/call-back")
// .scopes("resource", "userinfo", "all") // 授权允许的范围
// .authorizedGrantTypes("authorization_code", "refresh_token") // 授权类型,这里选择授权码模式
// .autoApprove(true) // 绝对自动授权,开启以后不用用户手动确认,不推荐,除非实在不想和用户交互
// ;
// 改为从数据库加载第三方平台信息,第三方接入量超过1W以后使用分页,小声bb:达到这个数量级有点难阿;
List<LoadThirdPartyPlatformsDto> thirdPartyPlatforms = adopApplicationService.getAllToLoadThirdPartyPlatformsDto();
// 获取内存写入对象,一定要在循环外创建,否则每次循环都是拿到一个新的,这样只有最后一个会生效
InMemoryClientDetailsServiceBuilder inMemory = clients.inMemory();
for (LoadThirdPartyPlatformsDto partyPlatform : thirdPartyPlatforms) {
ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder>.ClientBuilder builder = inMemory
.withClient(partyPlatform.getClientId().toString())
.secret(partyPlatform.getSecret())
.redirectUris(partyPlatform.getRedirectUri());
// 授权空间list
List<AdopScopeDto> scopes = partyPlatform.getScopes();
if (CollUtil.isNotEmpty(scopes)) {
builder.scopes(scopes.stream().map(AdopScope::getScopeCode).toArray(String[]::new))
.autoApprove(scopes.stream().filter(s -> s.getAutoStatus() == 1 && s.getId() != null).map(AdopScopeDto::getScopeCode).toArray(String[]::new));
}
// 授权类型list
List<AdopGrantType> grantTypes = partyPlatform.getGrantTypes();
if (CollUtil.isNotEmpty(grantTypes)) {
builder.authorizedGrantTypes(grantTypes.stream().filter(g -> g.getId() != null).map(AdopGrantType::getGrantTypeCode).toArray(String[]::new));
} else {
// 如果为空就默认授权码+刷新模式
builder.authorizedGrantTypes("authorization_code");
}
}
}
}
客户端认证时查询服务器获取结果
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final AdopApplicationService adopApplicationService;
private final ClientDetailServiceJDBCImpl jdbcClientDetailService;
public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, ClientDetailServiceJDBCImpl jdbcClientDetailService) {
this.passwordEncoder = passwordEncoder;
this.adopApplicationService = adopApplicationService;
this.jdbcClientDetailService = jdbcClientDetailService;
}
// 配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 加载应用客户端的信息
// 配置客户端详情加载器
final ClientDetailsServiceBuilder<?> serviceBuilder = clients.withClientDetails(jdbcClientDetailService);
final JdbcClientDetailsServiceBuilder jdbc = serviceBuilder.jdbc();
// 配置加密解密
jdbc.passwordEncoder(passwordEncoder);
// 配置数据源
jdbc.dataSource(new DruidDataSource());
}
}
实现org.springframework.security.oauth2.provider.ClientDetailsService;接口并重写他的loadClientByClientId接口,然后把这个对象注入到资源认证服务器配置中,并设置进withClientDetails中
这样在客户端验证的时候就会自动调用我们的实现方法,我们只需要在这里返回对应的ClientDetails就可以了
@Component
public class ClientDetailServiceJDBCImpl implements ClientDetailsService {
private final AdopApplicationService adopApplicationService;
public ClientDetailServiceJDBCImpl(AdopApplicationService adopApplicationService) {
this.adopApplicationService = adopApplicationService;
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
final LoadThirdPartyPlatformsDto appDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
.orElseThrow(() -> new RuntimeException("ClientId not found"));
return cpToClientDetails(appDto);
}
public ClientDetails cpToClientDetails(LoadThirdPartyPlatformsDto adopApplication) {
// 实现将AdopApplication对象转换为ClientDetails对象的逻辑
return new CustomClientDetails(adopApplication);
}
}
实现org.springframework.security.oauth2.provider.ClientDetails;接口即可定义一个自定义的客户端详情对象
@Data
@AllArgsConstructor
public class CustomClientDetails implements ClientDetails {
private LoadThirdPartyPlatformsDto clientInfo;
@Override
public String getClientId() {
return this.clientInfo.getClientId().toString();
}
@Override
public Set<String> getResourceIds() {
return null;
}
@Override
public boolean isSecretRequired() {
return true;
}
@Override
public String getClientSecret() {
return this.clientInfo.getSecret();
}
@Override
public boolean isScoped() {
return true;
}
// 返回允许的授权空间
@Override
public Set<String> getScope() {
final List<AdopScopeDto> scopes = this.clientInfo.getScopes();
return scopes.stream().map(AdopScopeDto::getScopeCode).collect(Collectors.toSet());
}
// 返回允许的授权类型
@Override
public Set<String> getAuthorizedGrantTypes() {
final TreeSet<String> set = new TreeSet<>();
set.add("authorization_code");
set.add("refresh_token");
return set;
}
// 回调地址
@Override
public Set<String> getRegisteredRedirectUri() {
final String redirectUri = this.clientInfo.getRedirectUri();
return new TreeSet<String>() {{
add(redirectUri);
}};
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public Integer getAccessTokenValiditySeconds() {
return null;
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return null;
}
// 是否自动授权
@Override
public boolean isAutoApprove(String scope) {
final Boolean enableAutoConfirm = this.clientInfo.getEnableAutoConfirm();
return enableAutoConfirm != null && enableAutoConfirm;
}
@Override
public Map<String, Object> getAdditionalInformation() {
return null;
}
}
用于配置每个资源访问所需的权限
// 资源服务配置
@Configuration
@EnableResourceServer // 启用资源服务
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private AdopScopeService adopScopeService;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
List<AdopScope> scopeList = adopScopeService.findAll();
for (AdopScope scope : scopeList) {
registry.antMatchers(scope.getScopeUri())
.access("#oauth2.hasAnyScope('"+scope.getScopeCode()+"')")
.and()
.requestMatchers().antMatchers(scope.getScopeUri());
}
http.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable();
// 旧版硬编码样例
// registry
// // 配置带资源域限制的资源信息
// .antMatchers("/resource/private/**").access("#oauth2.hasAnyScope('private')")
// .antMatchers("/resource/userInfo/**").access("#oauth2.hasAnyScope('userInfo')")
// .antMatchers("/resource/login/**").access("#oauth2.hasAnyScope('login')")
// .antMatchers("/resource/login/openId").access("#oauth2.hasAnyScope('login')")
// .and()
// // 匹配资源,对上面的资源进行匹配地址,配置在里面的资源将受到保护,必须全部认证才能访问
// // 上面配置了这个资源的访问权限。这里依然需要配置保护
// .requestMatchers()
// .antMatchers("/resource/private/**")
// .antMatchers("/resource/userInfo/**")
// .antMatchers("/resource/login/**")
// .antMatchers("/resource/login/openId")
// .and()
// // 指定任何请求,设r任何请求都需要授权以后才能访问
// .authorizeRequests().anyRequest().authenticated()
// .and().csrf().disable(); // 资源需要关闭这个,否则第三方拿到token以后依然无法访问会被拦截
}
}
最终目的就是验证后把UserDetails设置到SecurityContextHolder中
主要用于配置全局的访问控制,以及资源所有者的加载&登录方法
这里我们用到的流程是:主动配置加载和解密的Bean,最后通过默认的表单提交行为,或者主动触发 authenticationManager.authenticate()调用加载和校验最终实现的方法来进行用户详情对象的创建
想要定制的话可以自己添加过滤器,在喜欢的地方自己创建用户详情对象写入到SecurityContextHolder中完成身份的认证;
@Configuration
@EnableWebSecurity // 启动WebSecurity[可以写在配置类]
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomJwtTokenFilter customJwtTokenFilter;
private final UserDetailLoader userLoad;
public SecurityConfig(CustomJwtTokenFilter customJwtTokenFilter, UserDetailLoader userLoad) {
this.customJwtTokenFilter = customJwtTokenFilter;
this.userLoad = userLoad;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 单页面应用或者app可以选择关闭这个,只要不是基于会话的都可以
.cors().and() // 允许跨域
.authorizeRequests()// 配置认证请求
.antMatchers("/auth/login","/index.html") // 目前只开放鉴权入口;
.permitAll() // 对上面描述的匹配规则进行放行
// 切换到任何请求,设置都要进行认证之后才能访问
.anyRequest().authenticated();
//配置这个会造成user后的404响应,可能是因为配合了规则却没有配置后文
//http.and().requestMatchers().antMatchers("/user/**");
//http.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
http.formLogin().permitAll(); // 对表单认证进行放行,同时自定义登录验证路由
// 添加jwt过滤器到密码校验之前,在那之前完成jwt的校验和放入安全上下文对象
http.addFilterBefore(customJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 配置用户加载器和密码校验器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户加载类,以及加密方案
auth.userDetailsService(userLoad) // 用户加载类
// 这里不使用默认。使用一个自定义的方法
.passwordEncoder(new CustomJwtTokenEncoder());
}
// 当出现无法注入bean【AuthenticationManager】时添加,这个Bean用于主动调用框架的密码校验
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 配置加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 跨域配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*"); // 允许所有域访问
configuration.addAllowedMethod("*"); // 允许所有方法
configuration.addAllowedHeader("*"); // 允许所有头部
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
默认基于会话完成,我们可以在登录以后把这个对象设置好,在会话结束之前都可以保持登录
// 使用用户名密码创建一个用户密码对象,交给校验器校验
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 我们预先配置好的用户加载器和密码校验器这时候就会被调用
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 验证成功后得到安全上下文对象,设置到持有者中就可以了
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
登录后前端保存token,后端在每一次请求来的时候解析token,并把解析的内容(id,auth,role)创建成UserDetail设置到SecurityContextHolder中
public class JwtFilter implements Filter {
private final AdminJwtUtils jwtUtils;
public JwtFilter(AdminJwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Admin-Token");
if (StringUtils.isNotBlank(token)){
// 校验token
UserDetails user = jwtUtils.parseToken(token);
// 不为空即为校验通过
if (user!=null){
// 手动创建安全上下文,设置到线程域中
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
user,"", user.getAuthorities()
));
}
}
chain.doFilter(request, response);
}
}
客户端是获取access_token的时候,通过表单传递客户端的client_id和client_secret进行身份状态的保持的,
用户同意授权后,并成功兑换accessToken,再次申请相同权限会自动允许
默认使用的是basic auth的方式进行身份认证的
basic auth的认证规范是在请求头中设置Authorization值,
Value内容格式为Basic ${Base64.create(username:password)}
以下是JS代码示例
const username = 'your_username';
const password = 'your_password';
// 将用户名和密码以 "username:password" 的形式拼接,并进行 Base64 编码
const base64Credentials = btoa(`${username}:${password}`);
// 设置请求头,包含 Authorization 字段
const headers = new Headers({
'Authorization': `Basic ${base64Credentials}`,
'Content-Type': 'application/json', // 根据你的请求需要设置其他头部
});
// 构建请求对象
const requestOptions = {
method: 'GET', // 根据你的请求类型设置
headers: headers,
// 其他请求选项(例如:body)
};
// 发起请求
fetch('https://api.example.com/resource', requestOptions)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
在授权配置中重写configure(AuthorizationServerSecurityConfigurer security)添加我们自己定义的过滤器进行身份校验就可以了,校验通过以后同样创建一个UserDetail安全上下文到上下文持有者中就可以了,离谱,没想到和用户居然共用一个类
你可以把客户端的用户名和密码写在请求头里或者body里,然后取出来进行校验
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 配置自定义的客户端认证过滤器
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.addTokenEndpointAuthenticationFilter(customJwtTokenFilter);
}
}
https://baijiahao.baidu.com/s?id=1736936966974655693&wfr=spider&for=pc
授权表单的信息是基于Session保持的
也就是发起授权时保存了一个session在浏览器
然后表单提价的时候携带session进行提交,然后处理提交的表单
我们创建一个新的页面覆盖原来的/oauth/confirm_access就可以了
注意@SessionAttributes(“authorizationRequest”)一定不能少,表单是基于session维持会话的
这里我们用到了模板引擎
@RestController
@RequestMapping(value = "/oauth")
@SessionAttributes("authorizationRequest")
public class OauthController {
private final AdopApplicationService adopApplicationService;
private final AdopScopeService adopScopeService;
public OauthController(AdopApplicationService adopApplicationService, AdopScopeService adopScopeService) {
this.adopApplicationService = adopApplicationService;
this.adopScopeService = adopScopeService;
}
@RequestMapping(value = "/confirm_access")
public ModelAndView userConfirm(Model model) {
// 这里先提取一下我们传递过来的参数,例如客户端id,state,回调地址等
final AuthorizationRequest value = (AuthorizationRequest) model.getAttribute("authorizationRequest");
if (value == null) {
throw new RuntimeException("无法获取授权请求参数");
}
final String clientId = value.getClientId();
if (StringUtils.isBlank(clientId)) {
throw new RuntimeException("没有提供客户端参数");
}
// 查询一下客户端名称方便页面显示授权方
final LoadThirdPartyPlatformsDto clientDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
.orElseThrow(() -> new RuntimeException("客户端不存在"));
final Set<String> scope = value.getScope();
// set转换为list
final List<AdopScope> scopes = adopScopeService.findByCodes(new ArrayList<>(scope));
model.addAttribute("client", clientDto.getAppName());
model.addAttribute("scopeList", scopes);
return new ModelAndView("userConfirm.html");
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="container">
<h1>授权认证</h1>
<p th:text="'是否授权给'+${client}+'使用您的如下资源:'"></p>
<form id="confirmationForm" name="confirmationForm" action="/open-api/oauth/authorize" method="post"><input
name="user_oauth_approval" value="true" type="hidden">
<div id="atr" th:attr="scopeList = ${scopeList}"></div>
<ul class="scope-list">
<li class="scope-item" th:each="scopeItem : ${scopeList}">
<div class="form-group">
<span th:text="${scopeItem.scopeName}"></span>
<!-- 这里的name一定要是'scope.'+scope在资源服务注册的name-->
<span class="boxes">
<input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="true" checked="">允许
<input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="false">拒绝
</span>
</div>
</li>
</ul>
<label class="btn-container" ><input class="submit" name="authorize" value="授权" type="submit"></label>
</form>
</div>
</body>
</html>
<style>
body,html{
padding: 0;
margin: 0;
border: none;
}
body{
display: flex;
background-color: #efeefc;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
}
.container{
padding: 20px;
min-width: 400px;
border-radius: 10px;
background-color: #7e75ff;
box-shadow: 5px 5px 5px rgba(0,0,0,.1);
}
h1{
text-align: center;
}
#confirmationForm{
border: 1px;
position: relative;
}
.scope-list{
font-size: 18px;
}
.scope-item{
margin: 15px 0;
font-size: 16px;
}
.boxes{
flex-direction: row;
display: flex;
align-items: center;
font-size: 16px;
}
.btn-container{
display: block;
min-width: 100%;
text-align: center;
}
.submit{
bottom: 0px;
left: calc(50% - 100px);
border: none;
width: 200px;
height: 35px;
background-color: rgba(255,255,255,.8);
border-radius: 6px;
box-shadow: 5px 5px 5px rgba(0,0,0,.1);
}
</style>