使用安卓手机,通过NFC功能可以读取IC卡信息,。
IC卡有很多种卡标准,协议,等等,这些具体就不细讨论。
主要讨论,2种卡,一种是M1卡,另外是CPU卡。
1、 M1卡
M1卡是市面上比较常见的卡,一般的门禁卡。比较便宜。
M1卡的数据存储比较通用的是分扇区存储数据。默认是16个扇区,每个扇区4个数据块。
每个卡片都有一个ID号码(去买很多的卡的时候,可以要求厂商提供给你的每个卡ID是唯一的。)
0扇区的数据,是厂商写死的数据,包含卡面的ID号。
其他扇区的数据,可以自己去写,可以加密写。(密码为12位)
2、 CPU卡
CPU卡是加密卡,CPU卡还分为双界面卡和单界面卡。比较复杂。此次只说明,CPU卡的数据,通常是通过指令来读取数据。
3、android 的NFC功能
当安卓手机的NFC功能感应到 卡片的时候。会读取出该卡片支持的数据传输方式
android.nfc.tech.NfcA
android.nfc.tech.NfcB
android.nfc.tech.MifareClassic
android.nfc.tech.IsoDep
1、 普通的M1卡,会支持好几种数据传输方式,一般会有NfcA,MifareClassic等。如果支持MifareClassic,那就优先使用这种数据传输方式,获取数据。
2、 CPU卡的数据传输,会支持NfcA,IsoDep等,优先使用IsoDep
3、现在市面上出现了云解身份证,原理是通过NFC读取身份证的加密信息,通过API调用公安接口返回用户的证件信息。身份证通过NFC读取,只能通过NFCB的模式读取数据。
安卓开启NFC功能,一定先开启权限
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Smkpda"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<!--NFC权限-->
<uses-permission android:name="android.permission.NFC" />
<!-- 要求当前设备必须要有NFC芯片 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />
</manifest>
4、定义一个单例的NfcUtils 工具类
java
package com.sss.ssspda.common.nfc;
import android.app.Activity;
import android.content.Context;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.util.Log;
import android.widget.Toast;
/**
* Created by Administrator on 2023/11/10.
*/
public class NfcUtils {
private static final String TAG = "NfcUtils";
private static NfcAdapter mNfcAdapter;
//NFC功能,只能定义为单例模式,避免出现系统奔溃
private NfcUtils(){}
private static NfcUtils nfcUtils = null;
private static boolean isOpen = false;
/**
* 获取NFC的单例
* @return NfcUtils
*/
public static NfcUtils getInstance(){
if (nfcUtils == null){
synchronized (NfcUtils.class){
if (nfcUtils == null){
nfcUtils = new NfcUtils();
}
}
}
return nfcUtils;
}
/**
* 在onStart中检测是否支持nfc功能
* @param context 当前页面上下文
*/
public void onStartNfcAdapter(Context context){
mNfcAdapter = NfcAdapter.getDefaultAdapter(context);//设备的NfcAdapter对象
if(mNfcAdapter==null){//判断设备是否支持NFC功能
Toast.makeText(context,"设备不支持NFC功能!",Toast.LENGTH_SHORT).show();
return;
}
if (!mNfcAdapter.isEnabled()){//判断设备NFC功能是否打开
Toast.makeText(context,"请到系统设置中打开NFC功能!",Toast.LENGTH_SHORT).show();
return;
}
Log.d(TAG,"NFC is start");
}
/**
* 在onResume中开启nfc功能
* @param activity
*/
public void onResumeNfcAdapter(final Activity activity){
if (mNfcAdapter == null || !mNfcAdapter.isEnabled()) {
throw new RuntimeException("NFC is not start");
}
// mNfcAdapter.enableForegroundDispatch(this,mPendingIntent,null,null);//打开前台发布系统,使页面优于其它nfc处理.当检测到一个Tag标签就会执行mPendingItent
if (!isOpen) {
mNfcAdapter.enableReaderMode(activity, new NfcAdapter.ReaderCallback() {
@Override
public void onTagDiscovered(final Tag tag) {
//ByteArrayToHexString(tag.getId())即cardId
if (nfcListener != null)
(activity).runOnUiThread(new Runnable() {
@Override
public void run() {
nfcListener.doing(tag);
}
});
}
},
(NfcAdapter.FLAG_READER_NFC_A |
NfcAdapter.FLAG_READER_NFC_B |
NfcAdapter.FLAG_READER_NFC_F |
NfcAdapter.FLAG_READER_NFC_V |
NfcAdapter.FLAG_READER_NFC_BARCODE ),
null);
isOpen = true;
Log.d(TAG, "Resume");
}
}
/**
* 在onPause中关闭nfc功能
* @param activity
*/
public void onPauseNfcAdapter(Activity activity){
if(mNfcAdapter!=null && mNfcAdapter.isEnabled()){
if (isOpen){
mNfcAdapter.disableReaderMode(activity);
}
isOpen = false;
}
Log.d("myNFC","onPause");
}
private NfcListener nfcListener;
public void setNfcListener(NfcListener listener){
nfcListener = listener;
}
public String ByteArrayToHexString(byte[] inarray) {
int i, j, in;
String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A",
"B", "C", "D", "E", "F"};
String out = "";
for (j = 0; j < inarray.length; ++j) {
in = (int) inarray[j] & 0xff;
i = (in >> 4) & 0x0f;
out += hex[i];
i = in & 0x0f;
out += hex[i];
}
return out;
}
}
定义一个处理读卡事件的接口类
NfcListener
java
package com.sss.ssspda.common.nfc;
import android.nfc.Tag;
/**
* 自定义的NFC接口
*/
public interface NfcListener{
/**
* 用于扫到nfc后的后续操作
*/
void doing(Tag tag);
}
实现获取卡片数据交互
java
package com.sss.ssspda.common.nfc;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.NfcB;
import android.widget.Toast;
import java.io.IOException;
public class NfcReadHander {
/**
* 读M1卡的方法扇区,MifareClassic 类型
* @return
*
*/
public static String readMifareTag(Tag tag, MifareClassic mfc){
//读取TAG
try {
String metaInfo = "";
//操作之前,一点要先连接卡片通讯
mfc.connect();
int type = mfc.getType();//获取TAG的类型
int sectorCount = mfc.getSectorCount();//获取TAG中包含的扇区数
String typeS = "";
switch (type) {
case MifareClassic.TYPE_CLASSIC:
typeS = "TYPE_CLASSIC";
break;
case MifareClassic.TYPE_PLUS:
typeS = "TYPE_PLUS";
break;
case MifareClassic.TYPE_PRO:
typeS = "TYPE_PRO";
break;
case MifareClassic.TYPE_UNKNOWN:
typeS = "TYPE_UNKNOWN";
break;
}
metaInfo += "卡片类型:" + typeS + "\n共" + sectorCount + "个扇区\n共" + mfc.getBlockCount() + "个块\n存储空间: " + mfc.getSize() + "B\n";
System.out.println(metaInfo);
int blockIndex;
for(int i =0;i<16;i++){
try{
System.out.println(i);
metaInfo += "扇区:"+i+",key:";
boolean f = false;
byte[] keybyte = new byte[]{0x01, (byte) 0x11, (byte) 0x18,0x3F,0x12, (byte) 0x11};//样例的一个密码,要自己去获取,卡片商获取
//每个扇区的数据读取都可能不一样,每个扇区有的有数据,有的是加密数据,有的是空数据
//没加密扇区都会配置一个默认密码,开发包里面默认带有3种默认密码,
//这里代码只列出来使用A加密的方法,也有B加密方法authenticateSectorWithKeyB,要根据实际卡片加密方法来设置
if(mfc.authenticateSectorWithKeyA(i,MifareClassic.KEY_DEFAULT)){
metaInfo += "KEY_DEFAULT";
f = true;
}else if(mfc.authenticateSectorWithKeyA(i,MifareClassic.KEY_NFC_FORUM)){
metaInfo += ".KEY_NFC_FORUM";
f = true;
}else if(mfc.authenticateSectorWithKeyA(i,MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY)){
metaInfo += ".KEY_MIFARE_APPLICATION_DIRECTORY";
f = true;
}else if(mfc.authenticateSectorWithKeyA(i,keybyte)){
//这里的密码 keybyte,要自己和卡片提供商来获取,有可能是一个批次的卡片的密码都一样,也可能每个卡片的密码都不一样,这里的密码需要自己想办法获取。
metaInfo += ByteArrayToHexString(keybyte);
f = true;
}else{
}
metaInfo += "\n";
if(f){
//前面通过扇区界面进入了扇区里面,这里就是计算当前扇区的第一个块的块号码
int bnum = mfc.sectorToBlock(i);
//通过块号码读取块里面的数据
metaInfo += ByteArrayToHexString(mfc.readBlock(bnum))+"\n";
//每个扇区有4个块,依次读取
metaInfo +=ByteArrayToHexString(mfc.readBlock(bnum+1))+"\n";
metaInfo +=ByteArrayToHexString(mfc.readBlock(bnum+2))+"\n";
metaInfo +=ByteArrayToHexString(mfc.readBlock(bnum+3))+"\n";
}
}catch (Exception e){
e.printStackTrace();
}
}
return metaInfo;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mfc != null) {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static String ByteArrayToHexString(byte[] inarray) {
int i, j, in;
String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A",
"B", "C", "D", "E", "F"};
String out = "";
for (j = 0; j < inarray.length; ++j) {
in = (int) inarray[j] & 0xff;
i = (in >> 4) & 0x0f;
out += hex[i];
i = in & 0x0f;
out += hex[i];
}
return out;
}
//找卡片提供商获取的读卡指令
private static byte[] sheb = new byte[]{0x00, (byte) 0x14,0x04,0x00,0x0F,0x13,0x18,0x11,0x1E,0x13,0x18,0x2E,(byte) 0x19,(byte) 0xE1,(byte) 0xB1,(byte) 0xE1,(byte) 0x11
,(byte) 0x13,(byte) 0x15,(byte) 0xCF };
//找卡片提供商获取的读卡指令
private static byte[] JIAOTONG = new byte[]{0x00, (byte) 0x14, (byte) 0x04, (byte) 0x00, (byte) 0x18, (byte) 0x10, (byte) 0x10, (byte) 0x00
, (byte) 0x06, (byte) 0x12, (byte) 0x11, (byte) 0x11, (byte) 0x15};
//通过ISODEP读取数据
public static String readIsoDepTag(Tag tag, IsoDep isodep) {
String msg = "";
try{
//先连接
isodep.connect();
if(isodep.isConnected()){
//判断是否链接成功
}
msg += "sheb指令读取的数据:\n";
msg += transcMsg(isodep,sheb)+"\n";
msg += "JIAOTONG指令读取的数据:\n";
msg += transcMsg(isodep,JIAOTONG)+"\n";
}catch (Exception e){
}
return msg;
}
public static String transcMsg(IsoDep isodep,byte[] b) throws Exception{
return ByteArrayToHexString(isodep.transceive(b));
}
//nfcB类型,身份证就是此种传输方式
public static String readNfcBTag(Tag tag, NfcB nfcb) {
String msg = "";
try{
nfcb.connect();
msg += ByteArrayToHexString(nfcb.getApplicationData()) +"\n";
byte[] sd = new byte[]{0x05,0x00,0x00};
msg += ByteArrayToHexString(nfcb.transceive(sd)) +"\n";
msg += ByteArrayToHexString(nfcb.getApplicationData()) +"\n";
}catch (Exception e){
e.printStackTrace();
}
return msg;
}
}
调用主类
java
package com.sss.ssspda;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.NfcA;
import android.nfc.tech.NfcB;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.sss.ssspda.common.nfc.NfcListener;
import com.sss.ssspda.common.nfc.NfcReadHander;
import com.sss.ssspda.common.nfc.NfcUtils;
public class MainActivity extends AppCompatActivity implements NfcListener {
private TextView typetext;
private TextView datatext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
typetext = findViewById(R.id.typenfc);
datatext = findViewById(R.id.data);
nfcUtils.setNfcListener(this);
Log.d("myNFC","onPause");
}
public void Welcome1(View view) {
Toast.makeText(this, "按钮点击一下", Toast.LENGTH_SHORT).show();
}
public void Welcome2(View view) {
Toast.makeText(this, "撮了一下", Toast.LENGTH_SHORT).show();
}
private NfcUtils nfcUtils = NfcUtils.getInstance();
private TextView textView;
@Override
protected void onStart() {
super.onStart();
System.out.println("onStart................................");
nfcUtils.onStartNfcAdapter(this); //初始化Nfc对象
}
@Override
protected void onResume() {
super.onResume();
System.out.println("onResume................................");
nfcUtils.onResumeNfcAdapter(this); //activity激活的时候开始扫描
}
@Override
protected void onPause() {
super.onPause();
System.out.println("onPause................................");
nfcUtils.onPauseNfcAdapter(this); //activity切换到后台的时候停止扫描
}
@Override
public void doing(Tag tag) {
String tl[] = tag.getTechList();
for (String s:tl) {
System.out.println(s);
}
for (String s:tl) {
System.out.println("type:"+s);
if(s.equals("android.nfc.tech.MifareClassic")){
typetext.setText("MifareClassic");
String data = NfcReadHander.readMifareTag(tag,MifareClassic.get(tag));
datatext.setText(data);
break;
}else if(s.equals("android.nfc.tech.IsoDep")){
typetext.setText("IsoDep");
String data = NfcReadHander.readIsoDepTag(tag, IsoDep.get(tag));
datatext.setText(data);
break;
}else if(s.equals("android.nfc.tech.NfcB")){
typetext.setText("NfcB");
String data = NfcReadHander.readNfcBTag(tag, NfcB.get(tag));
datatext.setText(data);
break;
}
}System.out.println("onPause................................"+NfcReadHander.ByteArrayToHexString(tag.getId()));
}
布局文件
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="900sp"
android:onClick="Welcome1"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1054dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
android:background="#77CCB3"
android:text="读取类型"
android:textAlignment="center"
android:textSize="35sp" />
<TextView
android:id="@+id/typenfc"
android:layout_width="match_parent"
android:layout_height="77dp"
android:text="type"
android:textSize="25sp" />
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="59dp"
android:background="#6DCFC6"
android:text="数据"
android:textAlignment="center"
android:textSize="35sp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="613dp">
<TextView
android:id="@+id/data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="TextView"
android:textSize="15sp" />
</ScrollView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>