- 浏览: 938986 次
- 性别:
- 来自: 大连
文章分类
- 全部博客 (242)
- Flex (38)
- Java (15)
- iBatis (4)
- Spring (15)
- Oracle (4)
- Cognos (4)
- ActionScript (17)
- AIR (14)
- Box2D (1)
- Windows8 (3)
- AIR Mobile (4)
- Quartz (6)
- mybatis (7)
- CGLIB (1)
- Mobile (9)
- BlazeDS (3)
- IOS (8)
- FlashBuilder (1)
- Scout (1)
- Starling (5)
- APNS (3)
- Chrome (3)
- Windows (2)
- MySQL (15)
- Feathers (1)
- Tomcat (5)
- JavaPNS (1)
- SVN (2)
- XAMPP (1)
- Drupal (1)
- Linux (2)
- VSFTPD (1)
- jQuery (5)
- Discuz (2)
- Word (1)
- PHP (1)
- OFFICE (2)
- javascript (15)
- 微信 (1)
- 博客 (1)
- jquery mobile (5)
- nginx (1)
- memcached (1)
- maven (3)
- log4j (2)
- GitHub (2)
- html5 (3)
- WebSocket (2)
- Mac (11)
- Apache (1)
- JUnit (1)
- Eclipse (1)
- Openfire (1)
- HLS (1)
- Swift (6)
- Excel (2)
- IDE (4)
- NodeJS (8)
- 树莓 (3)
- CSS (2)
- PhoneGap (1)
- Angular.js (5)
- bootstrap (1)
- angular (5)
- React (1)
- Browserify (1)
- Ruby (1)
- WebService (1)
- CXF (1)
- redis (2)
- Dubbo (1)
- Jedis (1)
- solr (1)
- yeoman (1)
- grunt (1)
- bower (1)
- gulp (3)
- Git (2)
- angularJS (4)
- fastjson (1)
- Spring-Boot (1)
- Vue (1)
- Motan (1)
- Python (1)
最新评论
-
July01:
最近了解到一款StratoIO打印控件,功能如下:1、Html ...
NodeJS使用ipp协议打印 -
小“味”子:
不错不错,试了,是可以的
Mac下连接SQL Server客户端 -
akka_li:
我遇到这个问题了!我的原因是配置文件里写得各个包的xsd文件的 ...
Referenced file contains errors (http://www.springframework.org/schema...错误 -
迪伦少校:
我只想知道,你最后配置成功了吗?我这里怎么tomcat总是死呢 ...
关于 Nginx+Tomcat+Memcached做负载均衡加共享session -
LiYunpeng:
jun23100 写道我也遇到这个问题了,环境都是正确的,怎么 ...
关于HTML5请求WebSocket,404的问题
收藏列表
标题 | 标签 | 来源 | |
oauth | oauth | http://www.iteye.com | |
目前很多开放平台如新浪微博开放平台都在使用提供开放API接口供开发者使用,随之带来了第三方应用要到开放平台进行授权的问题,OAuth就是干这个的,OAuth2是OAuth协议的下一个版本,相比OAuth1,OAuth2整个授权流程更简单安全了,但不兼容OAuth1,具体可以到OAuth2官网http://oauth.net/2/查看,OAuth2协议规范可以参考http://tools.ietf.org/html/rfc6749。目前有好多参考实现供选择,可以到其官网查看下载。 本文使用Apache Oltu,其之前的名字叫Apache Amber ,是Java版的参考实现。使用文档可参考https://cwiki.apache.org/confluence/display/OLTU/Documentation。 OAuth角色 资源拥有者(resource owner):能授权访问受保护资源的一个实体,可以是一个人,那我们称之为最终用户;如新浪微博用户zhangsan; 资源服务器(resource server):存储受保护资源,客户端通过access token请求资源,资源服务器响应受保护资源给客户端;存储着用户zhangsan的微博等信息。 授权服务器(authorization server):成功验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端。 客户端(client):如新浪微博客户端weico、微格等第三方应用,也可以是它自己的官方应用;其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来/提交到服务器。“客户端”术语不代表任何特定实现(如应用运行在一台服务器、桌面、手机或其他设备)。 OAuth2协议流程 1、客户端从资源拥有者那请求授权。授权请求可以直接发给资源拥有者,或间接的通过授权服务器这种中介,后者更可取。 2、客户端收到一个授权许可,代表资源服务器提供的授权。 3、客户端使用它自己的私有证书及授权许可到授权服务器验证。 4、如果验证成功,则下发一个访问令牌。 5、客户端使用访问令牌向资源服务器请求受保护资源。 6、资源服务器会验证访问令牌的有效性,如果成功则下发受保护资源。 更多流程的解释请参考OAuth2的协议规范http://tools.ietf.org/html/rfc6749。 服务器端 本文把授权服务器和资源服务器整合在一起实现。 POM依赖 此处我们使用apache oltu oauth2服务端实现,需要引入authzserver(授权服务器依赖)和resourceserver(资源服务器依赖)。 Java代码 收藏代码 <dependency> <groupId>org.apache.oltu.oauth2</groupId> <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> <version>0.31</version> </dependency> <dependency> <groupId>org.apache.oltu.oauth2</groupId> <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> <version>0.31</version> </dependency> 其他的请参考pom.xml。 数据字典 用户(oauth2_user) 名称 类型 长度 描述 id bigint 10 编号 主键 username varchar 100 用户名 password varchar 100 密码 salt varchar 50 盐 客户端(oauth2_client) 名称 类型 长度 描述 id bigint 10 编号 主键 client_name varchar 100 客户端名称 client_id varchar 100 客户端id client_secret varchar 100 客户端安全key 用户表存储着认证/资源服务器的用户信息,即资源拥有者;比如用户名/密码;客户端表存储客户端的的客户端id及客户端安全key;在进行授权时使用。 表及数据SQL 具体请参考 sql/ shiro-schema.sql (表结构) sql/ shiro-data.sql (初始数据) 默认用户名/密码是admin/123456。 实体 具体请参考com.github.zhangkaitao.shiro.chapter17.entity包下的实体,此处就不列举了。 DAO 具体请参考com.github.zhangkaitao.shiro.chapter17.dao包下的DAO接口及实现。 Service 具体请参考com.github.zhangkaitao.shiro.chapter17.service包下的Service接口及实现。以下是出了基本CRUD之外的关键接口: Java代码 收藏代码 public interface UserService { public User createUser(User user);// 创建用户 public User updateUser(User user);// 更新用户 public void deleteUser(Long userId);// 删除用户 public void changePassword(Long userId, String newPassword); //修改密码 User findOne(Long userId);// 根据id查找用户 List<User> findAll();// 得到所有用户 public User findByUsername(String username);// 根据用户名查找用户 } Java代码 收藏代码 public interface ClientService { public Client createClient(Client client);// 创建客户端 public Client updateClient(Client client);// 更新客户端 public void deleteClient(Long clientId);// 删除客户端 Client findOne(Long clientId);// 根据id查找客户端 List<Client> findAll();// 查找所有 Client findByClientId(String clientId);// 根据客户端id查找客户端 Client findByClientSecret(String clientSecret);//根据客户端安全KEY查找客户端 } Java代码 收藏代码 public interface OAuthService { public void addAuthCode(String authCode, String username);// 添加 auth code public void addAccessToken(String accessToken, String username); // 添加 access token boolean checkAuthCode(String authCode); // 验证auth code是否有效 boolean checkAccessToken(String accessToken); // 验证access token是否有效 String getUsernameByAuthCode(String authCode);// 根据auth code获取用户名 String getUsernameByAccessToken(String accessToken);// 根据access token获取用户名 long getExpireIn();//auth code / access token 过期时间 public boolean checkClientId(String clientId);// 检查客户端id是否存在 public boolean checkClientSecret(String clientSecret);// 坚持客户端安全KEY是否存在 } 此处通过OAuthService实现进行auth code和access token的维护。 后端数据维护控制器 具体请参考com.github.zhangkaitao.shiro.chapter17.web.controller包下的IndexController、LoginController、UserController和ClientController,其用于维护后端的数据,如用户及客户端数据;即相当于后台管理。 授权控制器AuthorizeController Java代码 收藏代码 @Controller public class AuthorizeController { @Autowired private OAuthService oAuthService; @Autowired private ClientService clientService; @RequestMapping("/authorize") public Object authorize(Model model, HttpServletRequest request) throws URISyntaxException, OAuthSystemException { try { //构建OAuth 授权请求 OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request); //检查传入的客户端id是否正确 if (!oAuthService.checkClientId(oauthRequest.getClientId())) { OAuthResponse response = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(OAuthError.TokenResponse.INVALID_CLIENT) .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return new ResponseEntity( response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } Subject subject = SecurityUtils.getSubject(); //如果用户没有登录,跳转到登陆页面 if(!subject.isAuthenticated()) { if(!login(subject, request)) {//登录失败时跳转到登陆页面 model.addAttribute("client", clientService.findByClientId(oauthRequest.getClientId())); return "oauth2login"; } } String username = (String)subject.getPrincipal(); //生成授权码 String authorizationCode = null; //responseType目前仅支持CODE,另外还有TOKEN String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE); if (responseType.equals(ResponseType.CODE.toString())) { OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator()); authorizationCode = oauthIssuerImpl.authorizationCode(); oAuthService.addAuthCode(authorizationCode, username); } //进行OAuth响应构建 OAuthASResponse.OAuthAuthorizationResponseBuilder builder = OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_FOUND); //设置授权码 builder.setCode(authorizationCode); //得到到客户端重定向地址 String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI); //构建响应 final OAuthResponse response = builder.location(redirectURI).buildQueryMessage(); //根据OAuthResponse返回ResponseEntity响应 HttpHeaders headers = new HttpHeaders(); headers.setLocation(new URI(response.getLocationUri())); return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus())); } catch (OAuthProblemException e) { //出错处理 String redirectUri = e.getRedirectUri(); if (OAuthUtils.isEmpty(redirectUri)) { //告诉客户端没有传入redirectUri直接报错 return new ResponseEntity( "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND); } //返回错误消息(如?error=) final OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND) .error(e).location(redirectUri).buildQueryMessage(); HttpHeaders headers = new HttpHeaders(); headers.setLocation(new URI(response.getLocationUri())); return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus())); } } private boolean login(Subject subject, HttpServletRequest request) { if("get".equalsIgnoreCase(request.getMethod())) { return false; } String username = request.getParameter("username"); String password = request.getParameter("password"); if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { return false; } UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); return true; } catch (Exception e) { request.setAttribute("error", "登录失败:" + e.getClass().getName()); return false; } } } 如上代码的作用: 1、首先通过如http://localhost:8080/chapter17-server/authorize ?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login访问授权页面; 2、该控制器首先检查clientId是否正确;如果错误将返回相应的错误信息; 3、然后判断用户是否登录了,如果没有登录首先到登录页面登录; 4、登录成功后生成相应的auth code即授权码,然后重定向到客户端地址,如http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed;在重定向到的地址中会带上code参数(授权码),接着客户端可以根据授权码去换取access token。 访问令牌控制器AccessTokenController Java代码 收藏代码 @RestController public class AccessTokenController { @Autowired private OAuthService oAuthService; @Autowired private UserService userService; @RequestMapping("/accessToken") public HttpEntity token(HttpServletRequest request) throws URISyntaxException, OAuthSystemException { try { //构建OAuth请求 OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request); //检查提交的客户端id是否正确 if (!oAuthService.checkClientId(oauthRequest.getClientId())) { OAuthResponse response = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(OAuthError.TokenResponse.INVALID_CLIENT) .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return new ResponseEntity( response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } // 检查客户端安全KEY是否正确 if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) { OAuthResponse response = OAuthASResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT) .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return new ResponseEntity( response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE); // 检查验证类型,此处只检查AUTHORIZATION_CODE类型,其他的还有PASSWORD或REFRESH_TOKEN if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals( GrantType.AUTHORIZATION_CODE.toString())) { if (!oAuthService.checkAuthCode(authCode)) { OAuthResponse response = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(OAuthError.TokenResponse.INVALID_GRANT) .setErrorDescription("错误的授权码") .buildJSONMessage(); return new ResponseEntity( response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } } //生成Access Token OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator()); final String accessToken = oauthIssuerImpl.accessToken(); oAuthService.addAccessToken(accessToken, oAuthService.getUsernameByAuthCode(authCode)); //生成OAuth响应 OAuthResponse response = OAuthASResponse .tokenResponse(HttpServletResponse.SC_OK) .setAccessToken(accessToken) .setExpiresIn(String.valueOf(oAuthService.getExpireIn())) .buildJSONMessage(); //根据OAuthResponse生成ResponseEntity return new ResponseEntity( response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } catch (OAuthProblemException e) { //构建错误响应 OAuthResponse res = OAuthASResponse .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e) .buildJSONMessage(); return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus())); } } } 如上代码的作用: 1、首先通过如http://localhost:8080/chapter17-server/accessToken,POST提交如下数据:client_id= c1ebe466-1cdc-4bd3-ab69-77c3561b9dee& client_secret= d8346ea2-6017-43ed-ad68-19c0f971738b&grant_type=authorization_code&code=828beda907066d058584f37bcfd597b6&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login访问; 2、该控制器会验证client_id、client_secret、auth code的正确性,如果错误会返回相应的错误; 3、如果验证通过会生成并返回相应的访问令牌access token。 资源控制器UserInfoController Java代码 收藏代码 @RestController public class UserInfoController { @Autowired private OAuthService oAuthService; @RequestMapping("/userInfo") public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException { try { //构建OAuth资源请求 OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.QUERY); //获取Access Token String accessToken = oauthRequest.getAccessToken(); //验证Access Token if (!oAuthService.checkAccessToken(accessToken)) { // 如果不存在/过期了,返回未验证错误,需重新验证 OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm(Constants.RESOURCE_SERVER_NAME) .setError(OAuthError.ResourceResponse.INVALID_TOKEN) .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED); } //返回用户名 String username = oAuthService.getUsernameByAccessToken(accessToken); return new ResponseEntity(username, HttpStatus.OK); } catch (OAuthProblemException e) { //检查是否设置了错误码 String errorCode = e.getError(); if (OAuthUtils.isEmpty(errorCode)) { OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm(Constants.RESOURCE_SERVER_NAME) .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED); } OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm(Constants.RESOURCE_SERVER_NAME) .setError(e.getError()) .setErrorDescription(e.getDescription()) .setErrorUri(e.getUri()) .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、 oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(HttpStatus.BAD_REQUEST); } } } 如上代码的作用: 1、首先通过如http://localhost:8080/chapter17-server/userInfo? access_token=828beda907066d058584f37bcfd597b6进行访问; 2、该控制器会验证access token的有效性;如果无效了将返回相应的错误,客户端再重新进行授权; 3、如果有效,则返回当前登录用户的用户名。 Spring配置文件 具体请参考resources/spring*.xml,此处只列举spring-config-shiro.xml中的shiroFilter的filterChainDefinitions属性: Java代码 收藏代码 <property name="filterChainDefinitions"> <value> / = anon /login = authc /logout = logout /authorize=anon /accessToken=anon /userInfo=anon /** = user </value> </property> 对于oauth2的几个地址/authorize、/accessToken、/userInfo都是匿名可访问的。 其他源码请直接下载文档查看。 服务器维护 访问localhost:8080/chapter17-server/,登录后进行客户端管理和用户管理。 客户端管理就是进行客户端的注册,如新浪微博的第三方应用就需要到新浪微博开发平台进行注册;用户管理就是进行如新浪微博用户的管理。 对于授权服务和资源服务的实现可以参考新浪微博开发平台的实现: http://open.weibo.com/wiki/授权机制说明 http://open.weibo.com/wiki/微博API 客户端 客户端流程:如果需要登录首先跳到oauth2服务端进行登录授权,成功后服务端返回auth code,然后客户端使用auth code去服务器端换取access token,最好根据access token获取用户信息进行客户端的登录绑定。这个可以参照如很多网站的新浪微博登录功能,或其他的第三方帐号登录功能。 POM依赖 此处我们使用apache oltu oauth2客户端实现。 Java代码 收藏代码 <dependency> <groupId>org.apache.oltu.oauth2</groupId> <artifactId>org.apache.oltu.oauth2.client</artifactId> <version>0.31</version> </dependency> 其他的请参考pom.xml。 OAuth2Token 类似于UsernamePasswordToken和CasToken;用于存储oauth2服务端返回的auth code。 Java代码 收藏代码 public class OAuth2Token implements AuthenticationToken { private String authCode; private String principal; public OAuth2Token(String authCode) { this.authCode = authCode; } //省略getter/setter } OAuth2AuthenticationFilter 该filter的作用类似于FormAuthenticationFilter用于oauth2客户端的身份验证控制;如果当前用户还没有身份验证,首先会判断url中是否有code(服务端返回的auth code),如果没有则重定向到服务端进行登录并授权,然后返回auth code;接着OAuth2AuthenticationFilter会用auth code创建OAuth2Token,然后提交给Subject.login进行登录;接着OAuth2Realm会根据OAuth2Token进行相应的登录逻辑。 Java代码 收藏代码 public class OAuth2AuthenticationFilter extends AuthenticatingFilter { //oauth2 authc code参数名 private String authcCodeParam = "code"; //客户端id private String clientId; //服务器端登录成功/失败后重定向到的客户端地址 private String redirectUrl; //oauth2服务器响应类型 private String responseType = "code"; private String failureUrl; //省略setter protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = (HttpServletRequest) request; String code = httpRequest.getParameter(authcCodeParam); return new OAuth2Token(code); } protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return false; } protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { String error = request.getParameter("error"); String errorDescription = request.getParameter("error_description"); if(!StringUtils.isEmpty(error)) {//如果服务端返回了错误 WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription); return false; } Subject subject = getSubject(request, response); if(!subject.isAuthenticated()) { if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) { //如果用户没有身份验证,且没有auth code,则重定向到服务端授权 saveRequestAndRedirectToLogin(request, response); return false; } } //执行父类里的登录逻辑,调用Subject.login登录 return executeLogin(request, response); } //登录成功后的回调方法 重定向到成功页面 protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { issueSuccessRedirect(request, response); return false; } //登录失败后的回调 protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, ServletResponse response) { Subject subject = getSubject(request, response); if (subject.isAuthenticated() || subject.isRemembered()) { try { //如果身份验证成功了 则也重定向到成功页面 issueSuccessRedirect(request, response); } catch (Exception e) { e.printStackTrace(); } } else { try { //登录失败时重定向到失败页面 WebUtils.issueRedirect(request, response, failureUrl); } catch (IOException e) { e.printStackTrace(); } } return false; } } 该拦截器的作用: 1、首先判断有没有服务端返回的error参数,如果有则直接重定向到失败页面; 2、接着如果用户还没有身份验证,判断是否有auth code参数(即是不是服务端授权之后返回的),如果没有则重定向到服务端进行授权; 3、否则调用executeLogin进行登录,通过auth code创建OAuth2Token提交给Subject进行登录; 4、登录成功将回调onLoginSuccess方法重定向到成功页面; 5、登录失败则回调onLoginFailure重定向到失败页面。 OAuth2Realm Java代码 收藏代码 public class OAuth2Realm extends AuthorizingRealm { private String clientId; private String clientSecret; private String accessTokenUrl; private String userInfoUrl; private String redirectUrl; //省略setter public boolean supports(AuthenticationToken token) { return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token类型 } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); return authorizationInfo; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { OAuth2Token oAuth2Token = (OAuth2Token) token; String code = oAuth2Token.getAuthCode(); //获取 auth code String username = extractUsername(code); // 提取用户名 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, code, getName()); return authenticationInfo; } private String extractUsername(String code) { try { OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); OAuthClientRequest accessTokenRequest = OAuthClientRequest .tokenLocation(accessTokenUrl) .setGrantType(GrantType.AUTHORIZATION_CODE) .setClientId(clientId).setClientSecret(clientSecret) .setCode(code).setRedirectURI(redirectUrl) .buildQueryMessage(); //获取access token OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST); String accessToken = oAuthResponse.getAccessToken(); Long expiresIn = oAuthResponse.getExpiresIn(); //获取user info OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(userInfoUrl) .setAccessToken(accessToken).buildQueryMessage(); OAuthResourceResponse resourceResponse = oAuthClient.resource( userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class); String username = resourceResponse.getBody(); return username; } catch (Exception e) { throw new OAuth2AuthenticationException(e); } } } 此Realm首先只支持OAuth2Token类型的Token;然后通过传入的auth code去换取access token;再根据access token去获取用户信息(用户名),然后根据此信息创建AuthenticationInfo;如果需要AuthorizationInfo信息,可以根据此处获取的用户名再根据自己的业务规则去获取。 Spring shiro配置(spring-config-shiro.xml) Java代码 收藏代码 <bean id="oAuth2Realm" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm"> <property name="cachingEnabled" value="true"/> <property name="authenticationCachingEnabled" value="true"/> <property name="authenticationCacheName" value="authenticationCache"/> <property name="authorizationCachingEnabled" value="true"/> <property name="authorizationCacheName" value="authorizationCache"/> <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/> <property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/> <property name="accessTokenUrl" value="http://localhost:8080/chapter17-server/accessToken"/> <property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/> <property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/> </bean> 此OAuth2Realm需要配置在服务端申请的clientId和clientSecret;及用于根据auth code换取access token的accessTokenUrl地址;及用于根据access token换取用户信息(受保护资源)的userInfoUrl地址。 Java代码 收藏代码 <bean id="oAuth2AuthenticationFilter" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter"> <property name="authcCodeParam" value="code"/> <property name="failureUrl" value="/oauth2Failure.jsp"/> </bean> 此OAuth2AuthenticationFilter用于拦截服务端重定向回来的auth code。 Java代码 收藏代码 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/> <property name="successUrl" value="/"/> <property name="filters"> <util:map> <entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> / = anon /oauth2Failure.jsp = anon /oauth2-login = oauth2Authc /logout = logout /** = user </value> </property> </bean> 此处设置loginUrl为http://localhost:8080/chapter17-server/authorize ?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login";其会自动设置到所有的AccessControlFilter,如oAuth2AuthenticationFilter;另外/oauth2-login = oauth2Authc表示/oauth2-login地址使用oauth2Authc拦截器拦截并进行oauth2客户端授权。 测试 1、首先访问http://localhost:9080/chapter17-client/,然后点击登录按钮进行登录,会跳到如下页面: 2、输入用户名进行登录并授权; 3、如果登录成功,服务端会重定向到客户端,即之前客户端提供的地址http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,并带着auth code过去; 4、客户端的OAuth2AuthenticationFilter会收集此auth code,并创建OAuth2Token提交给Subject进行客户端登录; 5、客户端的Subject会委托给OAuth2Realm进行身份验证;此时OAuth2Realm会根据auth code换取access token,再根据access token获取受保护的用户信息;然后进行客户端登录。 到此OAuth2的集成就完成了,此处的服务端和客户端相对比较简单,没有进行一些异常检测,请参考如新浪微博进行相应API及异常错误码的设计。 示例源代码:https://github.com/zhangkaitao/shiro-example;可加群 231889722 探讨Spring/Shiro技术。 |
|||
数据抓取 HTMLUNIT | |||
最近在用Jsoup抓取某网站数据,可有些页面是ajax请求动态生成的,去群里问了一下,大神说模拟ajax请求即可。去网上搜索了一下,发现了这篇文章,拿过来先用着试试。转帖如下: 网上关于网络爬虫实现方式有很多种,但是很多都不支持Ajax,李兄说:模拟才是王道。确实,如果能够模拟一个没有界面的浏览器,还有什么不能做到的呢? 关于解析Ajax网站的框架也有不少,我选择了HtmlUnit,官方网站:http://htmlunit.sourceforge.net /,htmlunit可以说是一个Java版本的无界面浏览器,几乎无所不能,而且很多东西都封装得特别完美。这是这几天来积累下来的心血,记录一下。 package com.lanyotech.www.wordbank; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.util.List; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController; import com.gargoylesoftware.htmlunit.ScriptResult; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlOption; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlSelect; public class WorldBankCrawl { private static String TARGET_URL = "http://databank.worldbank.org/ddp/home.do"; public static void main(String[] args) throws FailingHttpStatusCodeException, MalformedURLException, IOException { //模拟一个浏览器 WebClient webClient = new WebClient(); //设置webClient的相关参数 webClient.setJavaScriptEnabled(true); webClient.setCssEnabled(false); webClient.setAjaxController(new NicelyResynchronizingAjaxController()); webClient.setTimeout(35000); webClient.setThrowExceptionOnScriptError(false); //模拟浏览器打开一个目标网址 HtmlPage rootPage= webClient.getPage(TARGET_URL); //获取第一个数据库 HtmlSelect hs = (HtmlSelect) rootPage.getElementById("lstCubes"); //按要求选择第一个数据库 hs.getOption(0).setSelected(true); //模拟点击Next按钮,跳转到第二个页面 System.out.println("正在跳转…"); //执行按钮出发的js事件 ScriptResult sr = rootPage.executeJavaScript("javascript:setCubeData(2,-1,4,'/ddp');"); //跳转到第二个页面,选择国家 HtmlPage countrySelect = (HtmlPage) sr.getNewPage(); //获得包含全部国家信息的选择框页面 HtmlPage framePage=(HtmlPage)countrySelect.getFrameByName("frmTree1″).getEnclosedPage(); //获得selectAll按钮,触发js事件 framePage.executeJavaScript("javascript:TransferListAll(‘countrylst','countrylstselected','no');SetSelectedCount(‘countrylstselected','tdcount');"); //获取Next按钮,触发js事件 ScriptResult electricityScriptResult = framePage.executeJavaScript("javascript:wrapperSetCube('/ddp')"); System.out.println("正在跳转…"); //跳转到下一个页面electricitySelect HtmlPage electricitySelect = (HtmlPage) electricityScriptResult.getNewPage(); //获得electricity选择的iframe HtmlPage electricityFrame = (HtmlPage) electricitySelect.getFrameByName("frmTree1″).getEnclosedPage(); //获得选择框 HtmlSelect seriesSelect = (HtmlSelect) electricityFrame.getElementById("countrylst"); //获得所有的选择框内容 List optionList = seriesSelect.getOptions(); //将指定的选项选中 optionList.get(1).setSelected(true); //模拟点击select按钮 electricityFrame.executeJavaScript("javascript:TransferList('countrylst','countrylstselected','no');SetSelectedCount('countrylstselected','tdcount');"); //获取选中后,下面的选择框 HtmlSelect electricitySelected = (HtmlSelect) electricityFrame.getElementById("countrylstselected"); List list = electricitySelected.getOptions(); //模拟点击Next按钮,跳转到选择时间的页面 ScriptResult timeScriptResult = electricityFrame.executeJavaScript("javascript:wrapperSetCube('/ddp')"); System.out.println("正在跳转…"); HtmlPage timeSelectPage = (HtmlPage) timeScriptResult.getNewPage(); //获取选中时间的选择框 timeSelectPage = (HtmlPage) timeSelectPage.getFrameByName("frmTree1″).getEnclosedPage(); //选中所有的时间 timeSelectPage.executeJavaScript("javascript:TransferListAll('countrylst','countrylstselected','no');SetSelectedCount('countrylstselected','tdcount');"); //点击Next按钮 ScriptResult exportResult = timeSelectPage.executeJavaScript("javascript:wrapperSetCube('/ddp')"); System.out.println("正在跳转…"); //转到export页面 HtmlPage exportPage = (HtmlPage) exportResult.getNewPage(); //点击页面上的Export按钮,进入下载页面 ScriptResult downResult = exportPage.executeJavaScript("javascript:exportData('/ddp' ,'EXT_BULK' ,'WDI_Time=51||WDI_Series=1||WDI_Ctry=244||' );"); System.out.println("正在跳转…"); HtmlPage downLoadPage = (HtmlPage) downResult.getNewPage(); //点击Excel图标,开始下载 ScriptResult downLoadResult = downLoadPage.executeJavaScript("javascript:exportData('/ddp','BULKEXCEL');"); //下载Excel文件 InputStream is = downLoadResult.getNewPage().getWebResponse().getContentAsStream(); OutputStream fos = new FileOutputStream("d://test.xls"); byte[] buffer=new byte[1024*30]; int len=-1; while((len=is.read(buffer))>0){ fos.write(buffer, 0, len); } fos.close(); fos.close(); System.out.println("Success!"); } } |
|||
js mobile | |||
sencha touch >kendo ui >jquery mobile |
|||
wechat_root | |||
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@include file="common/common.jsp" %> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1"> <title>WeChat Root 20131030</title> <script type="text/javascript"> NIUCHEURL = "http://1.weixinniuche1.duapp.com"; var HOST = window.location.host; //console.log(window.location.host); //console.log(window.location.pathname); //console.log(window.location.port); DOMAIN = "http://" + HOST + WEB_CONTENT;//url_temp.substring(0, url_temp.indexOf("/root.jsp")); var openId; var dealerId; var orderId; $(document).on("pageshow", "#homePage", function(event, data) { openId = "openIdTxt";//$("#openIdTxt").val(); dealerId = "";//$("#dealerIdTxt").val(); orderId = "";//$("#orderIdTxt").val(); }); function linkToNiuChe(key) { var toLink = DOMAIN; toLink += "/Menu?MsgType=event&Event=CLICK&EventKey=" + key + "&FromUserName=" + openId + "&dealerId=" + dealerId; //ajaxCallCore(toLink); callByXMLHttpRequest(toLink,key); /* if ("my_car_01" == key) {// "行车信息"; toLink += "/coreServlet?MsgType=event&Event=CLICK&EventKey=" + key + "&FromUserName=" + openId + "&dealerId=" + dealerId; ajaxCallCore(toLink); return; //toLink += "/travelInfo.do?flag=travelInfo&openID="+openId+"&dealerId="+dealerId; } else if ("my_car_02" == key) {// "驾驶行为"; } else if ("my_car_03" == key) {// "车辆体检"; toLink += "/discoverHealth.do?flag=cardiagnosis&openID=" + openId + "&dealerId=" + dealerId; } else if ("my_car_04" == key) {// "车辆位置"; toLink += "/carmessage/carMap.jsp";//?lng="+map.get("lNG").toString()+"&lat="+map.get("lAT").toString(); } else if ("my_car_05" == key) {// "维修历史"; toLink += "/repairRe.do?flag=repairrecord&openID=" + openId + "&dealerId=" + dealerId; } else if ("more_01" == key) {// "违章信息"; toLink += "/getBreakRegulRecMm.do?flag=breakregule&openID=" + openId + "&dealerId=" + dealerId; } else if ("more_02" == key) {// "二手车信息"; //toLink += "/usedCarListMm.do?flag=usedCar&openID="+openId+"&modelId="+mt.get("modelId")+"&modelName="+modelName+"&dealerId="+dealerId; } else if ("more_03" == key) {// "远程打火"; } else if ("more_04" == key) {// "切换车辆"; toLink += "/carBindDrmNum.do?flag=carbinddrm&openID=" + openId + "&dealerId=" + dealerId; } else if ("main_introduce" == key) {// "简介"; //toLink += } else if ("main_appointment" == key) {// "预约/查询"; //"/dealersInfo.do?flag=getDealersInfo&openID="+openId+"&dealerId="+dealerId; toLink += "/activeService.do?flag=activeService&openID=" + openId + "&orderID=" + orderId + "&dealerId=" + dealerId; } $.mobile.changePage(toLink); */ } function callByXMLHttpRequest(url,eventKey) { if (window.XMLHttpRequest){ req = new XMLHttpRequest(); } else if (window.ActiveXObject){ req = new ActiveXObject("Microsoft.XMLHTTP"); } req.open("Post",url,true); req.onreadystatechange = callback; var xmlStr = "<xml>" + "<FromUserName><![CDATA[" + openId + "]]></FromUserName>" + "<MsgType><![CDATA[event]]></MsgType>" + "<Event><![CDATA[CLICK]]></Event>" + "<EventKey><![CDATA[" + eventKey + "]]></EventKey>" + "</xml>"; req.send(xmlStr); } function callback(data) { if(data.currentTarget.readyState == "4") { if(data.currentTarget.status == "200") { resolveXML($.parseXML(data.currentTarget.responseText)); } else { alert("网络不稳定,请稍后再试!"); } } } function resolveXML(xmlDoc) { msgType = $(xmlDoc).find("MsgType").text(); var li_str; if(msgType == "text") { description = $(xmlDoc).find("Content").text(); li_str = '<li data-icon="false"><div data-role="button" style="cursor:default">'; li_str += '<pre style="text-align:left;word-wrap:break-word;">' + description + '</pre>'; li_str += '</div></li>'; } else { $(xmlDoc).find("item").each(function() { var element = $(this); title = element.find("Title").text(); description = element.find("Description").text(); picurl = element.find("PicUrl").text(); url = element.children("Url").text(); url = url.replace(NIUCHEURL,DOMAIN); console.log(element.find("Description").text()); }); li_str = '<li data-icon="false"><a href="' + url + '" target="" data-role="button" >'; li_str += '<h3 style="text-align:left;margin:0px">' + title + '</h3><br>'; li_str += '<pre style="text-align:left;word-wrap:break-word;">' + description + '</pre><br>'; li_str += '</a></li>'; } $("#msglist").append(li_str).trigger('create'); $("#msgContent").scrollTop( $("#msgContent")[0].scrollHeight); } </script> </head> <body> <!-- /page --> <div data-role="page" id="homePage"> <!-- /header --> <!-- <div data-role="header" data-theme="b"> <h1>WeChartRoot 20131030</h1> <a href="./WeChartRoot20131030.html" data-icon="home" data-iconpos="notext" data-direction="reverse" class="ui-btn-right jqm-home">Home</a> </div> --> <!-- /content --> <div data-role="content"> <!-- <div style="width: 100%"> <form> <label>OpenId</label> <input type="text" data-clear-btn="true" id="openIdTxt" value="openIdTest"> <label>DealerId</label> <input type="text" data-clear-btn="true" id="dealerIdTxt" value="dealerIdTest"> <label>OrderId</label> <input type="text" data-clear-btn="true" id="orderIdTxt" value="orderIdTest"> </form> </div> --> <div id="msgContent" style="width: 100%; height: 200px; float: left; overflow: auto"> <div style="position: relative; padding: 30px"> <ol data-role="listview" id="msglist"> </ol> </div> </div> <div style="width: 33%; float: left"> <ul data-role="listview" data-inset="true"> <li data-role="list-divider">我的座驾</li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('my_car_01')">行车信息</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('my_car_02')">驾驶行为</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('my_car_03')">车辆体检</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('my_car_04')">车辆位置</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('my_car_05')">维修历史</a></li> </ul> </div> <div style="width: .5%; float: left"> <p></p> </div> <div style="width: 33%; float: left"> <ul data-role="listview" data-inset="true"> <li data-role="list-divider">VIP尊享</li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('main_introduce')">简介</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('main_appointment')">预约/查询</a></li> <li></li> <li data-icon="false"><a href="javascript:window.location.href=DOMAIN+'/getLoginPage'" >身份验证</a></li> </ul> </div> <div style="width: .5%; float: left"> <p></p> </div> <div style="width: 33%; float: left"> <ul data-role="listview" data-inset="true"> <li data-role="list-divider">更多服务</li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('more_01')">违章信息</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('more_02')">二手车信息</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('more_03')">远程打火</a></li> <li data-icon="false"><a href="#" onClick="linkToNiuChe('more_04')">切换车辆</a></li> <!-- <li data-icon="false"><a href="#" onClick="linkToNiuChe('more_05')">关于我们</a></li> --> </ul> </div> </div> </div> </body> </html> |
|||
LOG4J | |||
log4j的官方wiki [url]http://wiki.apache.org/logging-log4j/Log4jXmlFormat[/url] [code="xml"] <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <param name="Target" value="System.out"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%-5p %c{1} - %m%n"/> </layout> </appender> <root> <priority value ="debug" /> <appender-ref ref="console" /> </root> </log4j:configuration> [/code] 这是官网给出的一个最基本的配置 [code="xml"] <root> <priority value="debug" /><!-- 日志中输出级别,这里输出debug级别以上的日志 debug、info、warn、error、fatal --> <appender-ref ref="CONSOLE" /><!-- 在控制台输出 --> <appender-ref ref="FILE" /><!-- 在文件输出 --> </root> [/code] 这里面配置了 当前项目的全局日志输出情况,以两种形式输出,控制台和文件 [color=red]---------记录日志的多种输出方式 start---------[/color] 这里面的CONSOLE和FILE,是引用,分别引用下面的代码 [code="xml"]<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"><!-- 控制台输出 --> <layout class="org.apache.log4j.PatternLayout"><!-- 输出布局模式 --> <param name="ConversionPattern" value="%d - %c -%-4r [%t] %-5p %x - %m%n" /><!-- 输出格式 --> </layout> <!--限制输出级别--> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="LevelMax" value="ERROR"/> <param name="LevelMin" value="ERROR"/> </filter> </appender>[/code] 者里面配置了在控制台输出的具体配置 使用了ConsoleAppender,另外布局使用了PatternLayout filter的意思就是使用了过滤,讲这一块的内容再通过过滤器来过滤一遍,分级别高低,在这区间内的可以显示出来 (注意这里面参数的大小写) 还有其他的输出,比如通过JDBC输出到数据库、通过SMTP发送邮件,还有下面一段代码输出到File文件等 布局模式也有多种,比如输出成html网页形式等,具体可以参看官网介绍 关于输出格式 %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL %r 输出自应用启动到输出该log信息耗费的毫秒数 %c 输出所属的类目,通常就是所在类的全名 %t 输出产生该日志事件的线程名 %n 输出一个回车换行符,Windows平台为“/r/n”,Unix平台为“/n” %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10) 下面是输出到文件的配置 [code="xml"] <appender name="FILE" class="org.apache.log4j.FileAppender"><!-- 文件模式输出 --> <param name="File" value="C:/log4j1.log"/><!-- 输出到的位置 --> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d - %c -%-4r [%t] %-5p %x - %m%n" /> </layout> </appender> [/code] [code="xml"] <!-- ========================== 输出方式说明================================ --> <!-- Log4j提供的appender有以下几种: --> <!-- org.apache.log4j.ConsoleAppender(控制台), --> <!-- org.apache.log4j.FileAppender(文件), --> <!-- org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件), --> <!-- org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件), --> <!-- org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方) --> <!-- ========================================================================== --> <!-- 输出到日志文件 --> <appender name="filelog_appender" class="org.apache.log4j.RollingFileAppender"> <!-- 设置File参数:日志输出文件名 --> <param name="File" value="log/testlog4jxml_all.log" /> <!-- 设置是否在重新启动服务时,在原有日志的基础添加新日志 --> <param name="Append" value="true" /> <!-- 设置文件大小 --> <param name="MaxFileSize" value="1MB" /> <!-- 设置文件备份 --> <param name="MaxBackupIndex" value="10000" /> <!-- 设置输出文件项目和格式 --> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p (%c:%L)- %m%n" /> </layout> </appender> <!-- 输出到日志文件 每天一个日志 --> <appender name="filelog_daily" class="org.apache.log4j.DailyRollingFileAppender"> <param name="File" value="log/daily.log" /> <param name="DatePattern" value="'daily.'yyyy-MM-dd'.log'" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%d{yyyy-MM-dd HH:mm:ss\} %-5p] [%t] (%c:%L) - %m%n" /> </layout> </appender> [/code] 以上分别是记录到文件的集中方式 http://www.cnblogs.com/tqsummer/archive/2010/08/26/1809232.html 以上代码则记录了,通过配置SMTP来发送日志的方式 [code="xml"] <appender name="DATABASE" class="org.apache.log4j.jdbc.JDBCAppender"> <param name="URL" value="jdbc:oracle:thin:@192.168.0.59:1521:oanet"/> <param name="driver" value="oracle.jdbc.driver.OracleDriver"/> <param name="user" value="hdczoa"/> <param name="password" value="system"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="INSERT INTO hdczoa.LOG4J(stamp,thread, info_level,class,message) VALUES ('%d', '%t', '%p', '%c', %m)" /> </layout> </appender> [/code] 以上代码则记录了通过配置JDBC来实现日志插入数据库 [color=red]---------记录日志的多种输出方式 end---------[/color] 关于category 和logger [code="xml"] <logger name="com.abc" additivity="false"> <level value="WARN" /> <appender-ref ref="CONSOLE" /> </logger> [/code] 这段代码是针对指定的包来设置日志输出情况的 其中的appender-ref同上 而category是和logger几乎相同的,logger继承于category,现在已经不提倡使用category了 log4j的配置大概也就这些,欢迎补充 |
|||
html edge code 配色 | |||
标签名 69 103 140 属性名 122 179 108 值 30 155 152 文字 0 2 178 注释 222 198 165 |
|||
sublime | |||
obsidian eclipse color theme |
|||
jquery.easy-pie-chart | |||
jquery.easy-pie-chart http://developer.51cto.com/art/201208/351584.htm http://www.elated.com/articles/snazzy-animated-pie-chart-html5-jquery/ |
|||
mac hotkey | |||
option + space == spotlight fn+left & fn + end == home & end command + shift + F == full screen option + W == Launchpad control + F2 == menubar |
|||
代码高亮 | |||
代码高亮 Syntax Highlighter CodeMirror https://code.google.com/p/as3syntaxhighlight/ https://code.google.com/p/as3-commons/source/browse/trunk/as3-commons-asblocks/src/test/actionscript/org/as3commons/asblocks/impl/CodeMirror.as?r=1284 http://labs.searchcoders.com/text/ http://marijn.haverbeke.nl/codemirror/index.html |
|||
Trade | |||
国内国外 股票、证券、基金 订单交易 订单 约定定以及失效管理 市场商品发行一览及管理 入出金管理 T+0、T+1……营业日交易一览 所有证券交易一览 个人及法人账户管理 营业日 手续费等其他小功能管理 |